diff --git a/README.md b/README.md index 9bbfae8e..a228b71d 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,18 @@ network_route { 'default': netmask => '0.0.0.0', network => 'default' } +``` + + For SLES: + +```sh +network_route { 'default': + ensure => 'present', + gateway => '10.0.2.2', + interface => 'eth0', + netmask => '0.0.0.0', + network => 'default' +} ``` Create resources on the fly with the `puppet resource` command: diff --git a/lib/puppet/provider/network_config/sles.rb b/lib/puppet/provider/network_config/sles.rb new file mode 100644 index 00000000..9814fa4a --- /dev/null +++ b/lib/puppet/provider/network_config/sles.rb @@ -0,0 +1,235 @@ +require 'puppetx/filemapper' + +Puppet::Type.type(:network_config).provide(:sles) do + # SLES network_config network scripts provider. + # + # This provider manages the contents of /etc/networks/ifcfg-* to + # manage non-volatile network configuration. + # + # @see https://documentation.suse.com/sles/15-SP1/html/SLES-all/cha-network.html#sec-network-manconf-files-ifcfg + + include PuppetX::FileMapper + + desc 'SLES network-scripts provider' + + confine osfamily: :suse + defaultfor osfamily: :suse + + has_feature :startmode + has_feature :provider_options + + # @return [String] The path to network-script directory on sles systems + SLES_SCRIPT_DIRECTORY = '/etc/sysconfig/network'.freeze # rubocop:disable Lint/ConstantDefinitionInBlock + + # The valid vlan ID range is 0-4095; 4096 is out of range + SLES_VLAN_RANGE_REGEX = %r{[1-3]?\d{1,3}|40[0-8]\d|409[0-5]}.freeze # rubocop:disable Lint/ConstantDefinitionInBlock + + # aliases are almost free game, sles rejects some, and max total length is 15 characters + # 15 minus at least 2 for the interface name, and a colon leaves 12 characters for the alias + SLES_ALIAS_REGEX = %r{.{1,12}(? 'eth1') + # prov.select_file # => '/etc/sysconfig/network/ifcfg-eth1' + # + def select_file + "#{SLES_SCRIPT_DIRECTORY}/ifcfg-#{name}" + end + + # Scan all files in the networking directory for interfaces + # + # @param script_dir [String] The path to the networking scripts, defaults to + # {#SLES_SCRIPT_DIRECTORY} + # + # @return [Array] All network-script config files on this machine. + # + # @example + # SLESProvider.target_files + # # => ['/etc/sysconfig/network/ifcfg-eth0', '/etc/sysconfig/network/ifcfg-eth1'] + def self.target_files(script_dir = SLES_SCRIPT_DIRECTORY) + entries = Dir.entries(script_dir).select { |entry| entry.match SCRIPT_REGEX } + entries.map { |entry| File.join(SLES_SCRIPT_DIRECTORY, entry) } + end + + # Convert a SLES network script into a hash + # + # This is a hook method that will be called by PuppetX::Filemapper + # + # @param [String] filename The path of the interfaces file being parsed + # @param [String] contents The contents of the given file + # + # @return [Array>] A single element array containing + # the key/value pairs of properties parsed from the file. + # + # @example + # SLESProvider.parse_file('/etc/sysconfig/network/ifcfg-eth0', #) + # # => [ + # # { + # # :name => 'eth0', + # # :ipaddress => '169.254.0.1', + # # :netmask => '255.255.0.0', + # # }, + # # ] + def self.parse_file(filename, contents) + # Split up the file into lines + lines = contents.split("\n") + # Strip out all comments + lines.map! { |line| line.sub(%r{#.*$}, '') } + # Remove all blank lines + lines.reject! { |line| line =~ %r{^\s*$} } + + pair_regex = %r{^\s*(.+?)\s*=\s*(.*)\s*$} + + # INFO: It is valid ifcfg format to include certain functions and strings these should not be reported as errors, but silently removed before parsing + # https://bugs.centos.org/bug_view_page.php?bug_id=475 + # Remove lines with no = sign + lines.select! { |line| line =~ %r{^.*=.*$} } + + # Convert the data into key/value pairs + pairs = lines.each_with_object({}) do |line, hash| + raise Puppet::Error, %(#{filename} is malformed; "#{line}" did not match "#{pair_regex}") unless line.match(pair_regex) do |m| + key = m[1].strip + val = m[2].strip + hash[key] = val + end + + hash + end + + props = munge(pairs) + + # TODO: remove duct tape for #13 + # + # The :family property is making less and less sense because it seems that + # ipv6 configuration should add new properties instead of trying to collide + # with the ipv4 addresses. But right now, the :inet property is never used + # and it's creating a change on each resource update. This is a patch until + # the :family property is ripped out. + # + # See https://github.com/adrienthebo/puppet-network/issues/13 for the full + # issue that caused this, and https://github.com/adrienthebo/puppet-network/issues/16 + # for the resolution. + # + props[:family] = :inet + + # If there is no DEVICE property in the interface configuration we retrieve + # the interface name from the file name itself + props[:name] = filename.split('ifcfg-')[1] unless props.key?(:name) + + # Set the field to the default to prevent changes to the resource + props[:onboot] = true + + # The FileMapper mixin expects an array of providers, so we return the + # single interface wrapped in an array + [props] + end + + # @api private + def self.munge(pairs) + props = {} + + # Unquote all values + pairs.each_pair do |key, val| + if (munged = val.gsub(%r{['"]}, '')) + pairs[key] = munged + end + end + + # For each interface attribute that we recognize it, add the value to the + # hash with our expected label + SLES_NAME_MAPPINGS.each_pair do |type_name, sles_name| + next unless (val = pairs[sles_name]) + + pairs.delete(sles_name) + props[type_name] = val + end + + # if we encounter VLAN_ID parameter, set the interface mode to :vlan + pairs.each_pair do |key, _val| + props[:mode] = :vlan if key == 'VLAN_ID' + end + pairs.delete('VLAN_ID') + + # mode is a property so it should always have a value + props[:mode] ||= :raw + + # For all of the remaining values, blindly toss them into the options hash. + props[:options] = pairs + + props[:method] = 'static' unless %w[none bootp dhcp].include? props[:method] + + props + end + + def self.format_file(filename, providers) + return '' if providers.empty? + + if providers.length > 1 + raise Puppet::DevError, + "Unable to support multiple interfaces [#{providers.map(&:name).join(',')}] in a single file #{filename}" + end + + provider = providers[0] + props = {} + + props = provider.options if provider.options && provider.options != :absent + + # Map everything to a flat hash + SLES_NAME_MAPPINGS.each_key do |type_name| + if (val = provider.send(type_name)) && val != :absent + props[type_name] = val + end + end + + # :mode does not exist in SLES_NAME_MAPPINGS so we have to fetch it manually + # note that the inverse operation is in .munge instead of parse_file + val = provider.send(:mode) + props['VLAN_ID'] = provider.options['VLAN_ID'] if val == :vlan + + pairs = unmunge props + + pairs.each_with_object('') do |(key, value), str| + str << %(#{key}=#{value}\n) + end + end + + def self.unmunge(props) + pairs = {} + + SLES_NAME_MAPPINGS.each_pair do |type_name, sles_name| + if (val = props[type_name]) + props.delete(type_name) + pairs[sles_name] = val + end + end + + pairs.merge! props + + pairs.each_pair do |key, val| + pairs[key] = %("#{val}") if val.is_a?(String) && val.match(%r{\s+}) + end + + pairs + end + + def self.post_flush_hook(filename) + File.chmod(0o644, filename) + end +end diff --git a/lib/puppet/provider/network_route/sles.rb b/lib/puppet/provider/network_route/sles.rb new file mode 100644 index 00000000..85b927f6 --- /dev/null +++ b/lib/puppet/provider/network_route/sles.rb @@ -0,0 +1,102 @@ +require 'ipaddr' +require 'puppetx/filemapper' + +Puppet::Type.type(:network_route).provide(:sles) do + # SLES network_route routes provider. + # + # This provider uses the filemapper mixin to map the routes file to a + # collection of network_route providers, and back. + # + # @see https://documentation.suse.com/sles/15-SP4/html/SLES-all/cha-network.html#sec-network-manconf-files-routes + + include PuppetX::FileMapper + + desc 'SLES style routes provider' + + confine osfamily: :suse + defaultfor osfamily: :suse + + has_feature :provider_options + + def select_file + "/etc/sysconfig/network/ifroute-#{interface}" + end + + def self.target_files(script_dir = '/etc/sysconfig/network') + entries = Dir.entries(script_dir).select { |entry| entry.match %r{ifroute-.*} } + entries.map { |entry| File.join(script_dir, entry) } + end + + def self.parse_file(_filename, contents) + routes = [] + lines = contents.split("\n") + lines.each do |line| + # Strip off any trailing comments + line.sub!(%r{#.*$}, '') + + if line =~ %r{^\s*#|^\s*$} + # Ignore comments and blank lines + next + end + + route = line.split(' ', 5) + raise Puppet::Error, 'Malformed SLES route file, cannot instantiate network_route resources' if route.length < 4 + + new_route = {} + + new_route[:gateway] = route[1] + new_route[:interface] = route[3] + new_route[:options] = route[4] if route[4] + + if ['default', '0.0.0.0'].include? route[0] + new_route[:name] = 'default' # FIXME: Must match :name in order for changes to be detected + new_route[:network] = 'default' + new_route[:netmask] = '0.0.0.0' + else + ip = IPAddr.new(route[0]) + netmask_addr = ip.prefix <= 32 ? '255.255.255.255' : 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + netmask = IPAddr.new("#{netmask_addr}/#{ip.prefix}") + new_route[:name] = "#{ip}/#{ip.prefix}" # FIXME: Must match :name in order for changes to be detected + new_route[:network] = ip.to_s + new_route[:netmask] = netmask.to_s + end + routes << new_route + end + + routes + end + + # Generate an array of sections + def self.format_file(_filename, providers) + contents = [] + contents << header + # Build routes + providers.sort_by(&:name).each do |provider| + %w[network netmask gateway interface].each do |prop| + raise Puppet::Error, "#{provider.name} is missing the required parameter '#{prop}'." if provider.send(prop).nil? + end + contents << if provider.network == 'default' + "#{provider.network} #{provider.gateway} - #{provider.interface}" + else + ip = IPAddr.new("#{provider.network}/#{provider.netmask}") + "#{ip}/#{ip.prefix} #{provider.gateway} - #{provider.interface}" + end + contents << (provider.options == :absent ? "\n" : " #{provider.options}\n") + end + contents.join + end + + def self.header + <<~HEADER + # HEADER: This file is being managed by puppet. Changes to + # HEADER: routes that are not being managed by puppet will persist; + # HEADER: however changes to routes that are being managed by puppet will + # HEADER: be overwritten. In addition, file order is NOT guaranteed. + # HEADER: Last generated at: #{Time.now} + HEADER + end + + def self.post_flush_hook(filename) + File.chmod(0o644, filename) + end +end diff --git a/lib/puppet/type/network_config.rb b/lib/puppet/type/network_config.rb index 7f5827f6..92b05e66 100644 --- a/lib/puppet/type/network_config.rb +++ b/lib/puppet/type/network_config.rb @@ -22,6 +22,8 @@ disruption as it may mean bringing down the interface for a short period. EOD + feature :startmode, 'The system can define startmode of an interface' + ensurable newparam(:name) do @@ -88,6 +90,11 @@ defaultto :true end + newproperty(:startmode, required_features: :startmode) do + desc 'Allow/disallow startmode support for this interface' + defaultto :auto + end + newparam(:reconfigure, required_features: :reconfigurable, parent: Puppet::Property::Boolean) do desc 'Reconfigure the interface after the configuration has been updated' end diff --git a/metadata.json b/metadata.json index d4a56f1e..9a100cfb 100644 --- a/metadata.json +++ b/metadata.json @@ -29,6 +29,13 @@ "operatingsystemrelease": [ "8" ] + }, + { + "operatingsystem": "SLES", + "operatingsystemrelease": [ + "12", + "15" + ] } ], "dependencies": [ diff --git a/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth0-dhcp b/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth0-dhcp new file mode 100644 index 00000000..c7063516 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth0-dhcp @@ -0,0 +1,4 @@ +# Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] +BOOTPROTO=dhcp +LLADDR=00:50:56:B2:00:1B +STARTMODE=auto diff --git a/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth0-static b/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth0-static new file mode 100644 index 00000000..b0c3dc75 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth0-static @@ -0,0 +1,5 @@ +BOOTPROTO=static +STARTMODE=auto +NETMASK=255.255.255.0 +IPADDR=10.0.1.27 +LLADDR=aa:bb:cc:dd:ee:ff diff --git a/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth1-dhcp b/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth1-dhcp new file mode 100644 index 00000000..c7063516 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth1-dhcp @@ -0,0 +1,4 @@ +# Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] +BOOTPROTO=dhcp +LLADDR=00:50:56:B2:00:1B +STARTMODE=auto diff --git a/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth1-simple b/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth1-simple new file mode 100644 index 00000000..2a5e7581 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/ifcfg-eth1-simple @@ -0,0 +1,2 @@ +BOOTPROTO=dhcp +STARTMODE=auto diff --git a/spec/fixtures/provider/network_config/sles_spec/ifcfg-lo b/spec/fixtures/provider/network_config/sles_spec/ifcfg-lo new file mode 100644 index 00000000..f3baa9fb --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/ifcfg-lo @@ -0,0 +1,6 @@ +IPADDR=127.0.0.1 +NETMASK=255.0.0.0 +# If you're having problems with gated making 127.0.0.0/8 a martian, +# you can change this to something else (255.255.255.255, for example) +BROADCAST=127.255.255.255 +STARTMODE=auto diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond0 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond0 new file mode 100644 index 00000000..a7b2956d --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond0 @@ -0,0 +1,8 @@ +STARTMODE=auto +#IPADDR=172.24.61.11 +#NETMASK=255.255.255.0 +BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" +BONDING_MASTER=yes +BONDING_SLAVE_0=eth0 +BONDING_SLAVE_1=eth1 +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1 new file mode 100644 index 00000000..e7c3233d --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1 @@ -0,0 +1,8 @@ +STARTMODE=auto +IPADDR=172.20.1.9 +NETMASK=255.255.255.0 +BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" +BONDING_MASTER=yes +BONDING_SLAVE_0=eth2 +BONDING_SLAVE_1=eth3 +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1.1001 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1.1001 new file mode 100644 index 00000000..d28f870e --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1.1001 @@ -0,0 +1,5 @@ +STARTMODE=auto +BONDING_MASTER=yes +NETMASK=255.255.255.0 +IPADDR=172.24.66.1 +VLAN_ID=1001 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1~ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1~ new file mode 100644 index 00000000..327ed28f --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1~ @@ -0,0 +1,9 @@ +DEVICE=bond1 +STARTMODE=auto +IPADDR=172.20.1.9 +NETMASK=255.255.255.0 +#GATEWAY=172.24.61.1 +BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" +BONDING_MASTER=yes +#USER_CTL=no +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0 new file mode 100644 index 00000000..bb6b2b3c --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0 @@ -0,0 +1,5 @@ +# Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet +LLADDR=00:12:79:91:28:1f +STARTMODE=hotplug +BOOTPROTO=none +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.1 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.1 new file mode 100644 index 00000000..bafba9d7 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.1 @@ -0,0 +1,5 @@ +BOOTPROTO="none" +MTU="9000" +STARTMODE=auto +INTERFACETYPE="Ethernet" +ETHERDEVICE="br1" diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.4095 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.4095 new file mode 100644 index 00000000..209bf937 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.4095 @@ -0,0 +1,6 @@ +BOOTPROTO=static +STARTMODE=auto +MTU="9000" +INTERFACETYPE="Ethernet" +ETHERDEVICE="br4095" +VLAN_ID=4095 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.4096 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.4096 new file mode 100644 index 00000000..d28a2cf9 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.4096 @@ -0,0 +1,6 @@ +BOOTPROTO=static +STARTMODE=auto +MTU="9000" +INTERFACETYPE="Ethernet" +ETHERDEVICE="br4095" +VLAN_ID=4096 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth1 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth1 new file mode 100644 index 00000000..24142f45 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth1 @@ -0,0 +1,5 @@ +# Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet +LLADDR=00:12:79:91:28:20 +STARTMODE=hotplug +BOOTPROTO=none +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth2 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth2 new file mode 100644 index 00000000..51840dc2 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth2 @@ -0,0 +1,5 @@ +# Intel Corporation 82571EB Gigabit Ethernet Controller +LLADDR=00:26:55:e9:33:c4 +STARTMODE=hotplug +BOOTPROTO=none +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth3 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth3 new file mode 100644 index 00000000..0e0fc4c0 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth3 @@ -0,0 +1,5 @@ +# Intel Corporation 82571EB Gigabit Ethernet Controller +LLADDR=00:26:55:e9:33:c5 +STARTMODE=hotplug +BOOTPROTO=none +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-vlan100 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-vlan100 new file mode 100644 index 00000000..21d2e53d --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-vlan100 @@ -0,0 +1,6 @@ +BOOTPROTO=static +STARTMODE=off +NETMASK=255.255.255.0 +IPADDR=172.24.61.11 +ETHERDEVICE=bond0 +VLAN_ID=100 diff --git a/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-vlan200 b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-vlan200 new file mode 100644 index 00000000..9f85e234 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-vlan200 @@ -0,0 +1,6 @@ +BOOTPROTO=static +STARTMODE=off +NETMASK=255.255.255.0 +IPADDR=172.24.62.1 +ETHERDEVICE=bond0 +VLAN_ID=200 diff --git a/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-bond0 b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-bond0 new file mode 100644 index 00000000..a7b2956d --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-bond0 @@ -0,0 +1,8 @@ +STARTMODE=auto +#IPADDR=172.24.61.11 +#NETMASK=255.255.255.0 +BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" +BONDING_MASTER=yes +BONDING_SLAVE_0=eth0 +BONDING_SLAVE_1=eth1 +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-bond1 b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-bond1 new file mode 100644 index 00000000..e7c3233d --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-bond1 @@ -0,0 +1,8 @@ +STARTMODE=auto +IPADDR=172.20.1.9 +NETMASK=255.255.255.0 +BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" +BONDING_MASTER=yes +BONDING_SLAVE_0=eth2 +BONDING_SLAVE_1=eth3 +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth0 b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth0 new file mode 100644 index 00000000..bb6b2b3c --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth0 @@ -0,0 +1,5 @@ +# Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet +LLADDR=00:12:79:91:28:1f +STARTMODE=hotplug +BOOTPROTO=none +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth1 b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth1 new file mode 100644 index 00000000..24142f45 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth1 @@ -0,0 +1,5 @@ +# Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet +LLADDR=00:12:79:91:28:20 +STARTMODE=hotplug +BOOTPROTO=none +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth2 b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth2 new file mode 100644 index 00000000..51840dc2 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth2 @@ -0,0 +1,5 @@ +# Intel Corporation 82571EB Gigabit Ethernet Controller +LLADDR=00:26:55:e9:33:c4 +STARTMODE=hotplug +BOOTPROTO=none +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth3 b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth3 new file mode 100644 index 00000000..0e0fc4c0 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth3 @@ -0,0 +1,5 @@ +# Intel Corporation 82571EB Gigabit Ethernet Controller +LLADDR=00:26:55:e9:33:c5 +STARTMODE=hotplug +BOOTPROTO=none +MTU=1500 diff --git a/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-vlan100 b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-vlan100 new file mode 100644 index 00000000..c1afce6f --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-vlan100 @@ -0,0 +1,6 @@ +ETHERDEVICE=bond0 +BOOTPROTO=static +STARTMODE=off +NETMASK=255.255.255.0 +IPADDR=172.24.61.11 +VLAN_ID=100 diff --git a/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-vlan200 b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-vlan200 new file mode 100644 index 00000000..0f0a84b9 --- /dev/null +++ b/spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-vlan200 @@ -0,0 +1,6 @@ +ETHERDEVICE=bond0 +BOOTPROTO=static +STARTMODE=off +NETMASK=255.255.255.0 +IPADDR=172.24.62.1 +VLAN_ID=200 diff --git a/spec/fixtures/provider/network_route/sles/advanced_routes b/spec/fixtures/provider/network_route/sles/advanced_routes new file mode 100644 index 00000000..08e61be1 --- /dev/null +++ b/spec/fixtures/provider/network_route/sles/advanced_routes @@ -0,0 +1,5 @@ +172.28.45.0/30 172.18.6.2 - eth0 table 200 +172.17.67.0/30 172.18.6.2 - vlan200 table 200 +10.10.10.0/30 172.18.6.2 - eth0 table 200 +default 10.0.0.1 - eth1 table 200 +2a01:4f8:211:9d5:53::/96 2a01:4f8:211:9d5::2 - vlan200 table 200 diff --git a/spec/fixtures/provider/network_route/sles/simple_routes b/spec/fixtures/provider/network_route/sles/simple_routes new file mode 100644 index 00000000..bc7c7b5d --- /dev/null +++ b/spec/fixtures/provider/network_route/sles/simple_routes @@ -0,0 +1,5 @@ +172.28.45.0/30 172.18.6.2 - eth0 +172.17.67.0/30 172.18.6.2 - vlan200 +10.10.10.0/30 172.18.6.2 - eth0 +default 10.0.0.1 - eth1 +2a01:4f8:211:9d5:53::/96 2a01:4f8:211:9d5::2 - vlan200 diff --git a/spec/unit/provider/network_config/redhat_spec.rb b/spec/unit/provider/network_config/redhat_spec.rb index 5b15ab60..9a768065 100644 --- a/spec/unit/provider/network_config/redhat_spec.rb +++ b/spec/unit/provider/network_config/redhat_spec.rb @@ -20,6 +20,10 @@ def fixture_data(file) it 'is hotpluggable' do expect(described_class.declared_feature?(:hotpluggable)).to be true end + + # it 'is not startmode' do + # expect(described_class.declared_feature?(:startmode)).to be false + # end end describe 'selecting files to parse' do diff --git a/spec/unit/provider/network_config/sles_spec.rb b/spec/unit/provider/network_config/sles_spec.rb new file mode 100644 index 00000000..362ec012 --- /dev/null +++ b/spec/unit/provider/network_config/sles_spec.rb @@ -0,0 +1,385 @@ +require 'spec_helper' +require 'rspec/its' + +describe Puppet::Type.type(:network_config).provider(:sles) do + subject { described_class } + + def fixture_path + File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_config', 'sles_spec') + end + + def fixture_file(file) + File.join(fixture_path, file) + end + + def fixture_data(file) + File.read(fixture_file(file)) + end + + describe 'provider features' do + it 'is not hotpluggable' do + expect(described_class.declared_feature?(:hotpluggable)).to be false + end + + it 'is startmode' do + expect(described_class.declared_feature?(:startmode)).to be true + end + end + + describe 'selecting files to parse' do + subject { described_class.target_files(network_scripts_path).map { |file| File.basename(file) } } + + let(:network_scripts_path) { fixture_file('network-scripts') } + + valid_files = %w[ifcfg-bond0 ifcfg-bond1 ifcfg-eth0 ifcfg-eth1 ifcfg-eth2 + ifcfg-eth3 ifcfg-vlan100 ifcfg-vlan200 + ifcfg-eth0.4095 ifcfg-bond1.1001] + + invalid_files = %w[.ifcfg-bond0.swp ifcfg-bond1~ ifcfg-vlan500.bak + ifcfg-eth0:my.alias.bak ifcfg-eth0.4096] + + valid_files.each do |file| + it { is_expected.to include file } + end + + invalid_files.each do |file| + it { is_expected.not_to include file } + end + end + + describe 'when parsing' do + describe 'the name' do + let(:data) { described_class.parse_file('ifcfg-eth0', fixture_data('ifcfg-eth0-dhcp'))[0] } + + it { expect(data[:name]).to eq('eth0') } + end + + describe 'the startmode property' do + let(:data) { described_class.parse_file('ifcfg-eth0', fixture_data('ifcfg-eth0-dhcp'))[0] } + + it { expect(data[:startmode]).to eq('auto') } + end + + describe 'the bootproto property' do + describe 'when dhcp' do + let(:data) { described_class.parse_file('ifcfg-eth0', fixture_data('ifcfg-eth0-dhcp'))[0] } + + it { expect(data[:method]).to eq('dhcp') } + end + + describe 'when static' do + let(:data) { described_class.parse_file('ifcfg-eth0', fixture_data('ifcfg-eth0-static'))[0] } + + it { expect(data[:method]).to eq('static') } + end + end + + describe 'a static interface' do + let(:data) { described_class.parse_file('eth0', fixture_data('ifcfg-eth0-static'))[0] } + + it { expect(data[:ipaddress]).to eq('10.0.1.27') } + it { expect(data[:netmask]).to eq('255.255.255.0') } + end + + describe 'the options property' do + let(:data) { described_class.parse_file('eth0', fixture_data('ifcfg-eth0-static'))[0] } + + it { expect(data[:options]['LLADDR']).to eq('aa:bb:cc:dd:ee:ff') } + end + + describe 'with no extra options' do + let(:data) { described_class.parse_file('eth0', fixture_data('ifcfg-eth1-simple'))[0] } + + it { expect(data[:options]).to eq({}) } + end + + describe 'complex configuration' do + let(:virbonding_path) { File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_config', 'sles_spec', 'virbonding') } + + before do + allow(described_class).to receive(:target_files).and_return(Dir["#{virbonding_path}/*"]) + end + + describe 'bond0' do + subject { described_class.instances.find { |i| i.name == 'bond0' } } + + its(:startmode) { is_expected.to eq('auto') } + its(:mtu) { is_expected.to eq('1500') } + + its(:options) do + is_expected.to eq( + 'BONDING_MASTER' => 'yes', + 'BONDING_MODULE_OPTS' => %(mode=4 miimon=100 xmit_hash_policy=layer3+4), + 'BONDING_SLAVE_0' => 'eth0', + 'BONDING_SLAVE_1' => 'eth1' + ) + end + end + + describe 'bond1' do + subject { described_class.instances.find { |i| i.name == 'bond1' } } + + its(:startmode) { is_expected.to eq('auto') } + its(:ipaddress) { is_expected.to eq('172.20.1.9') } + its(:netmask) { is_expected.to eq('255.255.255.0') } + its(:mtu) { is_expected.to eq('1500') } + + its(:options) do + is_expected.to eq( + 'BONDING_MASTER' => 'yes', + 'BONDING_MODULE_OPTS' => %(mode=4 miimon=100 xmit_hash_policy=layer3+4), + 'BONDING_SLAVE_0' => 'eth2', + 'BONDING_SLAVE_1' => 'eth3' + ) + end + end + + describe 'eth0' do + subject { described_class.instances.find { |i| i.name == 'eth0' } } + + its(:startmode) { is_expected.to eq('hotplug') } + its(:mtu) { is_expected.to eq('1500') } + its(:mode) { is_expected.to eq(:raw) } + + its(:options) do + is_expected.to eq( + 'LLADDR' => '00:12:79:91:28:1f' + ) + end + end + + describe 'eth1' do + subject { described_class.instances.find { |i| i.name == 'eth1' } } + + its(:startmode) { is_expected.to eq('hotplug') } + its(:mtu) { is_expected.to eq('1500') } + its(:mode) { is_expected.to eq(:raw) } + + its(:options) do + is_expected.to eq( + 'LLADDR' => '00:12:79:91:28:20' + ) + end + end + + describe 'eth2' do + subject { described_class.instances.find { |i| i.name == 'eth2' } } + + its(:startmode) { is_expected.to eq('hotplug') } + its(:mtu) { is_expected.to eq('1500') } + its(:mode) { is_expected.to eq(:raw) } + + its(:options) do + is_expected.to eq( + 'LLADDR' => '00:26:55:e9:33:c4' + ) + end + end + + describe 'eth3' do + subject { described_class.instances.find { |i| i.name == 'eth3' } } + + its(:startmode) { is_expected.to eq('hotplug') } + its(:mtu) { is_expected.to eq('1500') } + its(:mode) { is_expected.to eq(:raw) } + + its(:options) do + is_expected.to eq( + 'LLADDR' => '00:26:55:e9:33:c5' + ) + end + end + + describe 'vlan100' do + subject { described_class.instances.find { |i| i.name == 'vlan100' } } + + its(:ipaddress) { is_expected.to eq('172.24.61.11') } + its(:netmask) { is_expected.to eq('255.255.255.0') } + its(:startmode) { is_expected.to eq('off') } + its(:method) { is_expected.to eq('static') } + its(:mode) { is_expected.to eq(:vlan) } + + its(:options) do + is_expected.to eq( + 'ETHERDEVICE' => 'bond0' + ) + end + end + + describe 'vlan200' do + subject { described_class.instances.find { |i| i.name == 'vlan200' } } + + its(:ipaddress) { is_expected.to eq('172.24.62.1') } + its(:netmask) { is_expected.to eq('255.255.255.0') } + its(:startmode) { is_expected.to eq('off') } + its(:method) { is_expected.to eq('static') } + its(:mode) { is_expected.to eq(:vlan) } + + its(:options) do + is_expected.to eq( + 'ETHERDEVICE' => 'bond0' + ) + end + end + end + + describe 'interface.vlan_id vlan configuration' do + let(:network_scripts_path) { fixture_file('network-scripts') } + + before do + allow(described_class).to receive(:target_files).and_return(Dir["#{network_scripts_path}/*"]) + end + + describe 'eth0.4095' do + subject { described_class.instances.find { |i| i.name == 'eth0.4095' } } + + its(:startmode) { is_expected.to eq('auto') } + its(:method) { is_expected.to eq('static') } + its(:mtu) { is_expected.to eq('9000') } + its(:mode) { is_expected.to eq(:vlan) } + + its(:options) do + is_expected.to eq( + 'INTERFACETYPE' => 'Ethernet', + 'ETHERDEVICE' => 'br4095' + ) + end + end + end + + describe 'when DEVICE is not present' do + let(:data) { described_class.parse_file('ifcfg-eth1', fixture_data('ifcfg-eth1-dhcp'))[0] } + + it { expect(data[:name]).to eq('eth1') } + end + end + + describe 'when formatting resources' do + let(:eth0_provider) do + instance_double('eth0_provider', + name: 'eth0', + ensure: :present, + startmode: 'auto', + hotplug: true, + family: 'inet', + method: 'none', + ipaddress: '169.254.0.1', + netmask: '255.255.255.0', + mtu: '1500', + mode: nil, + options: {}) + end + + let(:eth0_1_provider) do + instance_double('eth0_1_provider', + name: 'eth0.1', + ensure: :present, + startmode: 'auto', + family: 'inet', + method: 'none', + ipaddress: '169.254.0.1', + netmask: '255.255.255.0', + mtu: '1500', + mode: :vlan, + options: {}) + end + + let(:eth1_provider) do + instance_double('eth1_provider', + name: 'eth1', + etherdevice: 'eth1', + ensure: :present, + startmode: 'off', + family: 'inet', + method: 'none', + ipaddress: :absent, + netmask: :absent, + mtu: :absent, + mode: :vlan, + options: { + 'ETHERDEVICE' => 'eth1' + }) + end + + let(:lo_provider) do + instance_double('lo_provider', + name: 'lo', + startmode: 'yes', + family: 'inet', + method: 'loopback', + ipaddress: nil, + netmask: nil, + mode: nil, + options: {}) + end + + let(:bond0_provider) do + instance_double('bond0_provider', + name: 'bond0', + startmode: 'auto', + hotplug: true, + ipaddress: '172.20.1.9', + netmask: '255.255.255.0', + method: 'static', + mtu: '1500', + mode: nil, + options: { + 'BONDING_OPTS' => %(mode=4 miimon=100 xmit_hash_policy=layer3+4) + }) + end + + it 'fails if multiple interfaces are flushed to one file' do + expect { described_class.format_file('filepath', [eth0_provider, lo_provider]) }.to raise_error Puppet::DevError, %r{multiple interfaces} + end + + describe 'with test interface eth0' do + let(:data) { described_class.format_file('filepath', [eth0_provider]) } + + it { expect(data).to match(%r{STARTMODE=auto}) } + it { expect(data).to match(%r{BOOTPROTO=none}) } + it { expect(data).to match(%r{IPADDR=169\.254\.0\.1}) } + it { expect(data).to match(%r{NETMASK=255\.255\.255\.0}) } + end + + describe 'with test interface eth0.1' do + let(:data) { described_class.format_file('filepath', [eth0_1_provider]) } + + it { expect(data).to match(%r{STARTMODE=auto}) } + it { expect(data).to match(%r{BOOTPROTO=none}) } + it { expect(data).to match(%r{IPADDR=169\.254\.0\.1}) } + it { expect(data).to match(%r{NETMASK=255\.255\.255\.0}) } + end + + describe 'with test interface eth1' do + let(:data) { described_class.format_file('filepath', [eth1_provider]) } + + it { expect(data).to match(%r{ETHERDEVICE=eth1}) } + it { expect(data).to match(%r{BOOTPROTO=none}) } + it { expect(data).to match(%r{STARTMODE=off}) } + it { expect(data).not_to match(%r{absent}) } + end + + describe 'with test interface bond0' do + let(:data) { described_class.format_file('filepath', [bond0_provider]) } + + it { expect(data).to match(%r{BONDING_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3\+4"}) } + end + end + + describe 'when flushing a dirty file' do + before do + allow(File).to receive(:chmod).with(0o644, '/not/a/real/file') + allow(File).to receive(:unlink) + allow(described_class).to receive(:perform_write) + end + + it do + described_class.dirty_file!('/not/a/real/file') + described_class.flush_file('/not/a/real/file') + end + + it 'is expected that it shouldnot have have unlinked the file' do + expect(File).not_to have_received(:unlink) + end + end +end diff --git a/spec/unit/provider/network_route/sles_spec.rb b/spec/unit/provider/network_route/sles_spec.rb new file mode 100644 index 00000000..c6259194 --- /dev/null +++ b/spec/unit/provider/network_route/sles_spec.rb @@ -0,0 +1,170 @@ +require 'spec_helper' + +describe Puppet::Type.type(:network_route).provider(:sles) do + def fixture_data(file) + basedir = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_route', 'sles') + File.read(File.join(basedir, file)) + end + + describe 'when parsing' do + describe 'a simple well formed file' do + let(:data) { described_class.parse_file('', fixture_data('simple_routes')) } + + it 'parses out normal ipv4 network routes' do + expect(data.find { |h| h[:name] == '172.17.67.0/30' }).to eq( + name: '172.17.67.0/30', + network: '172.17.67.0', + netmask: '255.255.255.252', + gateway: '172.18.6.2', + interface: 'vlan200' + ) + end + + it 'parses out ipv6 network routes' do + expect(data.find { |h| h[:name] == '2a01:4f8:211:9d5:53::/96' }).to eq( + name: '2a01:4f8:211:9d5:53::/96', + network: '2a01:4f8:211:9d5:53::', + netmask: 'ffff:ffff:ffff:ffff:ffff:ffff::', + gateway: '2a01:4f8:211:9d5::2', + interface: 'vlan200' + ) + end + + it 'parses out default routes' do + expect(data.find { |h| h[:name] == 'default' }).to eq( + name: 'default', + network: 'default', + netmask: '0.0.0.0', + gateway: '10.0.0.1', + interface: 'eth1' + ) + end + end + + describe 'an advanced, well formed file' do + let :data do + described_class.parse_file('', fixture_data('advanced_routes')) + end + + it 'parses out normal ipv4 network routes' do + expect(data.find { |h| h[:name] == '2a01:4f8:211:9d5:53::/96' }).to eq( + name: '2a01:4f8:211:9d5:53::/96', + network: '2a01:4f8:211:9d5:53::', + netmask: 'ffff:ffff:ffff:ffff:ffff:ffff::', + gateway: '2a01:4f8:211:9d5::2', + interface: 'vlan200', + options: 'table 200' + ) + end + + it 'parses out normal ipv6 network routes' do + expect(data.find { |h| h[:name] == '172.17.67.0/30' }).to eq( + name: '172.17.67.0/30', + network: '172.17.67.0', + netmask: '255.255.255.252', + gateway: '172.18.6.2', + interface: 'vlan200', + options: 'table 200' + ) + end + end + + describe 'an invalid file' do + it 'fails' do + expect do + described_class.parse_file('', "192.168.1.1/30 via\n") + end.to raise_error(%r{Malformed SLES route file}) + end + end + end + + describe 'when formatting' do + let :route1_provider do + instance_double( + 'route1_provider', + name: '172.17.67.0/30', + network: '172.17.67.0', + netmask: '30', + gateway: '172.18.6.2', + interface: 'vlan200', + options: 'table 200' + ) + end + + let :route2_provider do + instance_double( + 'lo_provider', + name: '172.28.45.0/30', + network: '172.28.45.0', + netmask: '30', + gateway: '172.18.6.2', + interface: 'eth0', + options: 'table 200' + ) + end + + let :defaultroute_provider do + instance_double( + 'defaultroute_provider', + name: 'default', + network: 'default', + netmask: '', + gateway: '10.0.0.1', + interface: 'eth1', + options: 'table 200' + ) + end + + let :nooptions_provider do + instance_double( + 'nooptions_provider', + name: 'default', + network: 'default', + netmask: '', + gateway: '10.0.0.1', + interface: 'eth2', + options: :absent + ) + end + + let :content do + described_class.format_file('', [route1_provider, route2_provider, defaultroute_provider, nooptions_provider]) + end + + describe 'writing the route line' do + describe 'For standard (non-default) routes' do + it 'writes a single line for the route' do + expect(content.scan(%r{^172.17.67.0/30 .*$}).length).to eq(1) + end + + it 'writes 6 fields' do + expect(content.scan(%r{^172.17.67.0/30 .*$}).first.split.length).to eq(6) + end + + it 'has the correct fields appended' do + expect(content.scan(%r{^172.17.67.0/30 .*$}).first).to include('172.17.67.0/30 172.18.6.2 - vlan200 table 200') + end + + it 'fails if the netmask property is not defined' do + allow(route2_provider).to receive(:netmask).and_return(nil) + expect { content }.to raise_exception(%r{is missing the required parameter 'netmask'}) + end + + it 'fails if the gateway property is not defined' do + allow(route2_provider).to receive(:gateway).and_return(nil) + expect { content }.to raise_exception(%r{is missing the required parameter 'gateway'}) + end + end + end + + describe 'for default routes' do + it 'has the correct fields appended' do + expect(content.scan(%r{^default .*$}).first).to include('default 10.0.0.1 - eth1') + end + + it 'does not contain the word absent when no options are defined' do + expect(content).not_to match(%r{absent}) + end + end + end +end