├── spec ├── fixtures │ └── provider │ │ ├── network_config │ │ ├── sles_spec │ │ │ ├── ifcfg-eth1-simple │ │ │ ├── ifcfg-eth0-static │ │ │ ├── network-scripts │ │ │ │ ├── ifcfg-eth0.1 │ │ │ │ ├── ifcfg-bond1.1001 │ │ │ │ ├── ifcfg-vlan200 │ │ │ │ ├── ifcfg-eth0.4095 │ │ │ │ ├── ifcfg-eth0.4096 │ │ │ │ ├── ifcfg-vlan100 │ │ │ │ ├── ifcfg-eth2 │ │ │ │ ├── ifcfg-eth3 │ │ │ │ ├── ifcfg-eth0 │ │ │ │ ├── ifcfg-eth1 │ │ │ │ ├── ifcfg-bond1 │ │ │ │ ├── ifcfg-bond0 │ │ │ │ └── ifcfg-bond1~ │ │ │ ├── ifcfg-eth0-dhcp │ │ │ ├── ifcfg-eth1-dhcp │ │ │ ├── virbonding │ │ │ │ ├── ifcfg-vlan200 │ │ │ │ ├── ifcfg-vlan100 │ │ │ │ ├── ifcfg-eth2 │ │ │ │ ├── ifcfg-eth3 │ │ │ │ ├── ifcfg-eth0 │ │ │ │ ├── ifcfg-eth1 │ │ │ │ ├── ifcfg-bond1 │ │ │ │ └── ifcfg-bond0 │ │ │ └── ifcfg-lo │ │ ├── redhat_spec │ │ │ ├── eth1-simple │ │ │ ├── network-scripts │ │ │ │ ├── ifcfg-bond1.1001 │ │ │ │ ├── ifcfg-eth0 │ │ │ │ ├── ifcfg-eth1 │ │ │ │ ├── ifcfg-eth2 │ │ │ │ ├── ifcfg-eth3 │ │ │ │ ├── ifcfg-vlan200 │ │ │ │ ├── ifcfg-vlan300 │ │ │ │ ├── ifcfg-vlan400 │ │ │ │ ├── ifcfg-vlan500 │ │ │ │ ├── ifcfg-vlan500.bak │ │ │ │ ├── ifcfg-vlan100:0 │ │ │ │ ├── ifcfg-eth0.0 │ │ │ │ ├── ifcfg-eth0.1 │ │ │ │ ├── ifcfg-eth0.4095 │ │ │ │ ├── ifcfg-eth0.4096 │ │ │ │ ├── ifcfg-vlan100 │ │ │ │ ├── ifcfg-eth0:10000000 │ │ │ │ ├── ifcfg-eth0:alias.bak │ │ │ │ ├── ifcfg-eth0:my.alias │ │ │ │ ├── ifcfg-bond1 │ │ │ │ ├── ifcfg-bond1~ │ │ │ │ └── ifcfg-bond0 │ │ │ ├── eth0-static │ │ │ ├── eth1-dhcp │ │ │ ├── eth0-dhcp │ │ │ ├── virbonding │ │ │ │ ├── vlan200 │ │ │ │ ├── vlan300 │ │ │ │ ├── vlan400 │ │ │ │ ├── vlan500 │ │ │ │ ├── eth0 │ │ │ │ ├── eth1 │ │ │ │ ├── eth2 │ │ │ │ ├── eth3 │ │ │ │ ├── vlan100_0 │ │ │ │ ├── vlan100 │ │ │ │ ├── bond0 │ │ │ │ └── bond1 │ │ │ ├── eth0-hotplug │ │ │ ├── eth0-nohotplug │ │ │ └── lo │ │ └── interfaces_spec │ │ │ ├── loopback │ │ │ ├── single_interface_dhcp │ │ │ ├── iface_whitespace │ │ │ ├── two_interface_dhcp │ │ │ ├── single_interface_options │ │ │ ├── jessie_source_stanza │ │ │ ├── single_interface_static │ │ │ ├── flush_lo_eth0_dhcp │ │ │ ├── two_interfaces_static_vlan │ │ │ └── three_interface_static │ │ └── network_route │ │ ├── redhat │ │ ├── local_routes │ │ ├── simple_routes │ │ └── advanced_routes │ │ ├── sles │ │ ├── simple_routes │ │ └── advanced_routes │ │ └── routes_spec │ │ ├── advanced_routes │ │ └── simple_routes ├── spec_helper_methods.rb ├── travis_rspec.rb ├── classes │ └── bond │ │ └── setup_spec.rb ├── spec_helper_acceptance.rb ├── functions │ ├── compact_hash_spec.rb │ └── network │ │ └── compact_hash_spec.rb ├── unit │ ├── facter │ │ ├── network_primary_ip_spec.rb │ │ ├── network_primary_interface_spec.rb │ │ └── network_nexthop_ip_spec.rb │ ├── type │ │ ├── network_route_spec.rb │ │ └── network_config_spec.rb │ └── provider │ │ ├── network_route │ │ ├── sles_spec.rb │ │ ├── routes_spec.rb │ │ └── redhat_spec.rb │ │ └── network_config │ │ ├── sles_spec.rb │ │ └── interfaces_spec.rb ├── acceptance │ └── network_route_spec.rb ├── spec_helper.rb └── defines │ ├── bond_spec.rb │ └── bond │ ├── debian_spec.rb │ └── redhat_spec.rb ├── .msync.yml ├── .github ├── labeler.yml ├── workflows │ ├── labeler.yml │ ├── ci.yml │ ├── release.yml │ └── prepare_release.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── release.yml ├── .puppet-lint.rc ├── .rubocop.yml ├── lib ├── puppet_x │ └── voxpupuli │ │ └── utils.rb ├── facter │ └── network.rb └── puppet │ ├── parser │ └── functions │ │ └── compact_hash.rb │ ├── type │ ├── network_route.rb │ └── network_config.rb │ └── provider │ ├── network_route │ ├── sles.rb │ ├── routes.rb │ └── redhat.rb │ └── network_config │ ├── sles.rb │ ├── redhat.rb │ └── interfaces.rb ├── .sync.yml ├── .fixtures.yml ├── templates └── bond │ └── opts-redhat.erb ├── .editorconfig ├── .gitignore ├── functions └── compact_hash.pp ├── manifests ├── bond │ ├── setup.pp │ ├── redhat.pp │ └── debian.pp ├── init.pp └── bond.pp ├── .pmtignore ├── Gemfile ├── Rakefile ├── .overcommit.yml ├── metadata.json ├── .rubocop_todo.yml ├── README.md └── LICENSE /spec/fixtures/provider/network_config/sles_spec/ifcfg-eth1-simple: -------------------------------------------------------------------------------- 1 | BOOTPROTO=dhcp 2 | STARTMODE=auto 3 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/eth1-simple: -------------------------------------------------------------------------------- 1 | BOOTPROTO=dhcp 2 | ONBOOT=yes 3 | DEVICE=eth1 4 | HOTPLUG=yes 5 | -------------------------------------------------------------------------------- /.msync.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | modulesync_config_version: '10.4.0' 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/ifcfg-eth0-static: -------------------------------------------------------------------------------- 1 | BOOTPROTO=static 2 | STARTMODE=auto 3 | NETMASK=255.255.255.0 4 | IPADDR=10.0.1.27 5 | LLADDR=aa:bb:cc:dd:ee:ff 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.1: -------------------------------------------------------------------------------- 1 | BOOTPROTO="none" 2 | MTU="9000" 3 | STARTMODE=auto 4 | INTERFACETYPE="Ethernet" 5 | ETHERDEVICE="br1" 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-bond1.1001: -------------------------------------------------------------------------------- 1 | VLAN=yes 2 | DEVICE=bond1.1001 3 | BOOTPROTO=static 4 | NETMASK=255.255.255.0 5 | IPADDR=172.24.66.1 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/ifcfg-eth0-dhcp: -------------------------------------------------------------------------------- 1 | # Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] 2 | BOOTPROTO=dhcp 3 | LLADDR=00:50:56:B2:00:1B 4 | STARTMODE=auto 5 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/ifcfg-eth1-dhcp: -------------------------------------------------------------------------------- 1 | # Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] 2 | BOOTPROTO=dhcp 3 | LLADDR=00:50:56:B2:00:1B 4 | STARTMODE=auto 5 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1.1001: -------------------------------------------------------------------------------- 1 | STARTMODE=auto 2 | BONDING_MASTER=yes 3 | NETMASK=255.255.255.0 4 | IPADDR=172.24.66.1 5 | VLAN_ID=1001 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/eth0-static: -------------------------------------------------------------------------------- 1 | DEVICE=eth0 2 | BOOTPROTO=none 3 | ONBOOT=yes 4 | NETMASK=255.255.255.0 5 | IPADDR=10.0.1.27 6 | USERCTL=no 7 | NM_CONTROLLED=no 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/eth1-dhcp: -------------------------------------------------------------------------------- 1 | # Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] 2 | BOOTPROTO=dhcp 3 | DHCPCLASS= 4 | HWADDR=00:50:56:B2:00:1B 5 | ONBOOT=yes 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-vlan200: -------------------------------------------------------------------------------- 1 | ETHERDEVICE=bond0 2 | BOOTPROTO=static 3 | STARTMODE=off 4 | NETMASK=255.255.255.0 5 | IPADDR=172.24.62.1 6 | VLAN_ID=200 7 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_route/redhat/local_routes: -------------------------------------------------------------------------------- 1 | 10.0.0.0/8 via 10.0.0.1 dev eth0 2 | 192.168.0.0/16 via 10.0.0.1 table n0-hc 3 | local 10.0.0.2 dev eth0 proto 66 scope host table local 4 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-vlan200: -------------------------------------------------------------------------------- 1 | BOOTPROTO=static 2 | STARTMODE=off 3 | NETMASK=255.255.255.0 4 | IPADDR=172.24.62.1 5 | ETHERDEVICE=bond0 6 | VLAN_ID=200 7 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-vlan100: -------------------------------------------------------------------------------- 1 | ETHERDEVICE=bond0 2 | BOOTPROTO=static 3 | STARTMODE=off 4 | NETMASK=255.255.255.0 5 | IPADDR=172.24.61.11 6 | VLAN_ID=100 7 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.4095: -------------------------------------------------------------------------------- 1 | BOOTPROTO=static 2 | STARTMODE=auto 3 | MTU="9000" 4 | INTERFACETYPE="Ethernet" 5 | ETHERDEVICE="br4095" 6 | VLAN_ID=4095 7 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0.4096: -------------------------------------------------------------------------------- 1 | BOOTPROTO=static 2 | STARTMODE=auto 3 | MTU="9000" 4 | INTERFACETYPE="Ethernet" 5 | ETHERDEVICE="br4095" 6 | VLAN_ID=4096 7 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-vlan100: -------------------------------------------------------------------------------- 1 | BOOTPROTO=static 2 | STARTMODE=off 3 | NETMASK=255.255.255.0 4 | IPADDR=172.24.61.11 5 | ETHERDEVICE=bond0 6 | VLAN_ID=100 7 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | skip-changelog: 6 | - head-branch: ['^release-*', 'release'] 7 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/eth0-dhcp: -------------------------------------------------------------------------------- 1 | # Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] 2 | DEVICE=eth0 3 | BOOTPROTO=dhcp 4 | DHCPCLASS= 5 | HWADDR=00:50:56:B2:00:1B 6 | ONBOOT=yes 7 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth2: -------------------------------------------------------------------------------- 1 | # Intel Corporation 82571EB Gigabit Ethernet Controller 2 | LLADDR=00:26:55:e9:33:c4 3 | STARTMODE=hotplug 4 | BOOTPROTO=none 5 | MTU=1500 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth3: -------------------------------------------------------------------------------- 1 | # Intel Corporation 82571EB Gigabit Ethernet Controller 2 | LLADDR=00:26:55:e9:33:c5 3 | STARTMODE=hotplug 4 | BOOTPROTO=none 5 | MTU=1500 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth2: -------------------------------------------------------------------------------- 1 | # Intel Corporation 82571EB Gigabit Ethernet Controller 2 | LLADDR=00:26:55:e9:33:c4 3 | STARTMODE=hotplug 4 | BOOTPROTO=none 5 | MTU=1500 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth3: -------------------------------------------------------------------------------- 1 | # Intel Corporation 82571EB Gigabit Ethernet Controller 2 | LLADDR=00:26:55:e9:33:c5 3 | STARTMODE=hotplug 4 | BOOTPROTO=none 5 | MTU=1500 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth0: -------------------------------------------------------------------------------- 1 | # Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet 2 | LLADDR=00:12:79:91:28:1f 3 | STARTMODE=hotplug 4 | BOOTPROTO=none 5 | MTU=1500 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-eth1: -------------------------------------------------------------------------------- 1 | # Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet 2 | LLADDR=00:12:79:91:28:20 3 | STARTMODE=hotplug 4 | BOOTPROTO=none 5 | MTU=1500 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth0: -------------------------------------------------------------------------------- 1 | # Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet 2 | LLADDR=00:12:79:91:28:1f 3 | STARTMODE=hotplug 4 | BOOTPROTO=none 5 | MTU=1500 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-eth1: -------------------------------------------------------------------------------- 1 | # Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet 2 | LLADDR=00:12:79:91:28:20 3 | STARTMODE=hotplug 4 | BOOTPROTO=none 5 | MTU=1500 6 | -------------------------------------------------------------------------------- /.puppet-lint.rc: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | --fail-on-warnings 5 | --no-parameter_documentation-check 6 | --no-parameter_types-check 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | inherit_from: .rubocop_todo.yml 6 | inherit_gem: 7 | voxpupuli-test: rubocop.yml 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/vlan200: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan200 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.62.1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/vlan300: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan300 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.63.1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/vlan400: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan400 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.64.1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/vlan500: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan500 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.65.1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/eth0-hotplug: -------------------------------------------------------------------------------- 1 | # Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] 2 | DEVICE=eth0 3 | BOOTPROTO=dhcp 4 | DHCPCLASS= 5 | HWADDR=00:50:56:B2:00:1B 6 | ONBOOT=yes 7 | HOTPLUG=yes 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/eth0-nohotplug: -------------------------------------------------------------------------------- 1 | # Advanced Micro Devices [AMD] 79c970 [PCnet32 LANCE] 2 | DEVICE=eth0 3 | BOOTPROTO=dhcp 4 | DHCPCLASS= 5 | HWADDR=00:50:56:B2:00:1B 6 | ONBOOT=yes 7 | HOTPLUG=no 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/eth0: -------------------------------------------------------------------------------- 1 | # Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet 2 | DEVICE=eth0 3 | ONBOOT=yes 4 | HWADDR=00:12:79:91:28:1f 5 | MTU=1500 6 | SLAVE=yes 7 | MASTER=bond0 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/eth1: -------------------------------------------------------------------------------- 1 | # Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet 2 | DEVICE=eth1 3 | ONBOOT=yes 4 | HWADDR=00:12:79:91:28:20 5 | MASTER=bond0 6 | SLAVE=yes 7 | MTU=1500 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/eth2: -------------------------------------------------------------------------------- 1 | # Intel Corporation 82571EB Gigabit Ethernet Controller 2 | DEVICE=eth2 3 | HWADDR=00:26:55:e9:33:c4 4 | ONBOOT=yes 5 | MTU=1500 6 | SLAVE=yes 7 | MASTER=bond1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/eth3: -------------------------------------------------------------------------------- 1 | # Intel Corporation 82571EB Gigabit Ethernet Controller 2 | DEVICE=eth3 3 | HWADDR=00:26:55:e9:33:c5 4 | MTU=1500 5 | ONBOOT=yes 6 | MASTER=bond1 7 | SLAVE=yes 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/vlan100_0: -------------------------------------------------------------------------------- 1 | #VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | #VLAN=yes 3 | DEVICE=vlan100:0 4 | #PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.61.12 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth0: -------------------------------------------------------------------------------- 1 | # Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet 2 | DEVICE=eth0 3 | ONBOOT=yes 4 | HWADDR=00:12:79:91:28:1f 5 | MTU=1500 6 | SLAVE=yes 7 | MASTER=bond0 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth1: -------------------------------------------------------------------------------- 1 | # Broadcom Corporation NetXtreme BCM5704 Gigabit Ethernet 2 | DEVICE=eth1 3 | ONBOOT=yes 4 | HWADDR=00:12:79:91:28:20 5 | MASTER=bond0 6 | SLAVE=yes 7 | MTU=1500 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth2: -------------------------------------------------------------------------------- 1 | # Intel Corporation 82571EB Gigabit Ethernet Controller 2 | DEVICE=eth2 3 | HWADDR=00:26:55:e9:33:c4 4 | ONBOOT=yes 5 | MTU=1500 6 | SLAVE=yes 7 | MASTER=bond1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth3: -------------------------------------------------------------------------------- 1 | # Intel Corporation 82571EB Gigabit Ethernet Controller 2 | DEVICE=eth3 3 | HWADDR=00:26:55:e9:33:c5 4 | MTU=1500 5 | ONBOOT=yes 6 | MASTER=bond1 7 | SLAVE=yes 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-vlan200: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan200 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.62.1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-vlan300: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan300 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.63.1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-vlan400: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan400 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.64.1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-vlan500: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan500 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.65.1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-vlan500.bak: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan500 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.65.1 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-vlan100:0: -------------------------------------------------------------------------------- 1 | #VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | #VLAN=yes 3 | DEVICE=vlan100:0 4 | #PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.61.12 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/vlan100: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan100 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.61.11 8 | GATEWAY=172.24.61.1 9 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth0.0: -------------------------------------------------------------------------------- 1 | DEVICE="eth0.0" 2 | VLAN="yes" 3 | BOOTPROTO="none" 4 | IPV6INIT="no" 5 | MTU="9000" 6 | NM_CONTROLLED="no" 7 | ONBOOT="yes" 8 | TYPE="Ethernet" 9 | BRIDGE="br1" 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth0.1: -------------------------------------------------------------------------------- 1 | DEVICE="eth0.1" 2 | VLAN="yes" 3 | BOOTPROTO="none" 4 | IPV6INIT="no" 5 | MTU="9000" 6 | NM_CONTROLLED="no" 7 | ONBOOT="yes" 8 | TYPE="Ethernet" 9 | BRIDGE="br1" 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_route/sles/simple_routes: -------------------------------------------------------------------------------- 1 | 172.28.45.0/30 172.18.6.2 - eth0 2 | 172.17.67.0/30 172.18.6.2 - vlan200 3 | 10.10.10.0/30 172.18.6.2 - eth0 4 | default 10.0.0.1 - eth1 5 | 2a01:4f8:211:9d5:53::/96 2a01:4f8:211:9d5::2 - vlan200 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth0.4095: -------------------------------------------------------------------------------- 1 | DEVICE="eth0.4095" 2 | VLAN="yes" 3 | BOOTPROTO="none" 4 | IPV6INIT="no" 5 | MTU="9000" 6 | NM_CONTROLLED="no" 7 | ONBOOT="yes" 8 | TYPE="Ethernet" 9 | BRIDGE="br4095" 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth0.4096: -------------------------------------------------------------------------------- 1 | DEVICE="eth0.4096" 2 | VLAN="yes" 3 | BOOTPROTO="none" 4 | IPV6INIT="no" 5 | MTU="9000" 6 | NM_CONTROLLED="no" 7 | ONBOOT="yes" 8 | TYPE="Ethernet" 9 | BRIDGE="br4095" 10 | -------------------------------------------------------------------------------- /lib/puppet_x/voxpupuli/utils.rb: -------------------------------------------------------------------------------- 1 | module PuppetX 2 | module Voxpupuli 3 | module Utils 4 | def self.try(catch = ArgumentError, nope = false) 5 | yield 6 | rescue catch 7 | nope 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-vlan100: -------------------------------------------------------------------------------- 1 | VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD 2 | VLAN=yes 3 | DEVICE=vlan100 4 | PHYSDEV=bond0 5 | BOOTPROTO=static 6 | NETMASK=255.255.255.0 7 | IPADDR=172.24.61.11 8 | GATEWAY=172.24.61.1 9 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_route/redhat/simple_routes: -------------------------------------------------------------------------------- 1 | 172.28.45.0/30 via 172.18.6.2 dev eth0 2 | 172.17.67.0/30 via 172.18.6.2 dev vlan200 3 | 10.10.10.0/30 via 172.18.6.2 dev eth0 4 | default via 10.0.0.1 dev eth1 5 | 2a01:4f8:211:9d5:53::/96 via 2a01:4f8:211:9d5::2 dev vlan200 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth0:10000000: -------------------------------------------------------------------------------- 1 | DEVICE="eth0:10000000" 2 | IPADDR="10.10.10.10" 3 | NETMASK="255.255.255.0" 4 | BOOTPROTO="none" 5 | IPV6INIT="no" 6 | MTU="9000" 7 | NM_CONTROLLED="no" 8 | ONBOOT="yes" 9 | TYPE="Ethernet" 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth0:alias.bak: -------------------------------------------------------------------------------- 1 | DEVICE="eth0:my.alias" 2 | IPADDR="10.10.10.10" 3 | NETMASK="255.255.255.0" 4 | BOOTPROTO="none" 5 | IPV6INIT="no" 6 | MTU="9000" 7 | NM_CONTROLLED="no" 8 | ONBOOT="yes" 9 | TYPE="Ethernet" 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-eth0:my.alias: -------------------------------------------------------------------------------- 1 | DEVICE="eth0:my.alias" 2 | IPADDR="10.10.10.10" 3 | NETMASK="255.255.255.0" 4 | BOOTPROTO="none" 5 | IPV6INIT="no" 6 | MTU="9000" 7 | NM_CONTROLLED="no" 8 | ONBOOT="yes" 9 | TYPE="Ethernet" 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-bond1: -------------------------------------------------------------------------------- 1 | STARTMODE=auto 2 | IPADDR=172.20.1.9 3 | NETMASK=255.255.255.0 4 | BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 5 | BONDING_MASTER=yes 6 | BONDING_SLAVE_0=eth2 7 | BONDING_SLAVE_1=eth3 8 | MTU=1500 9 | -------------------------------------------------------------------------------- /.sync.yml: -------------------------------------------------------------------------------- 1 | --- 2 | spec/spec_helper.rb: 3 | mock_with: ':rspec' 4 | spec_overrides: "require 'spec_helper_methods'" 5 | Gemfile: 6 | optional: 7 | ':test': 8 | - gem: 'ipaddress' 9 | - gem: 'rspec-its' 10 | spec/spec_helper_acceptance.rb: 11 | unmanaged: false 12 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/ifcfg-lo: -------------------------------------------------------------------------------- 1 | IPADDR=127.0.0.1 2 | NETMASK=255.0.0.0 3 | # If you're having problems with gated making 127.0.0.0/8 a martian, 4 | # you can change this to something else (255.255.255.255, for example) 5 | BROADCAST=127.255.255.255 6 | STARTMODE=auto 7 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1: -------------------------------------------------------------------------------- 1 | STARTMODE=auto 2 | IPADDR=172.20.1.9 3 | NETMASK=255.255.255.0 4 | BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 5 | BONDING_MASTER=yes 6 | BONDING_SLAVE_0=eth2 7 | BONDING_SLAVE_1=eth3 8 | MTU=1500 9 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/virbonding/ifcfg-bond0: -------------------------------------------------------------------------------- 1 | STARTMODE=auto 2 | #IPADDR=172.24.61.11 3 | #NETMASK=255.255.255.0 4 | BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 5 | BONDING_MASTER=yes 6 | BONDING_SLAVE_0=eth0 7 | BONDING_SLAVE_1=eth1 8 | MTU=1500 9 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/bond0: -------------------------------------------------------------------------------- 1 | DEVICE=bond0 2 | ONBOOT=yes 3 | #IPADDR=172.24.61.11 4 | #NETMASK=255.255.255.0 5 | #GATEWAY=172.24.61.1 6 | #NO_ALIASROUTING=yes 7 | BONDING_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 8 | #USER_CTL=no 9 | MTU=1500 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/virbonding/bond1: -------------------------------------------------------------------------------- 1 | DEVICE=bond1 2 | ONBOOT=yes 3 | IPADDR=172.20.1.9 4 | NETMASK=255.255.255.0 5 | #GATEWAY=172.24.61.1 6 | #NO_ALIASROUTING=yes 7 | BONDING_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 8 | #USER_CTL=no 9 | MTU=1500 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond0: -------------------------------------------------------------------------------- 1 | STARTMODE=auto 2 | #IPADDR=172.24.61.11 3 | #NETMASK=255.255.255.0 4 | BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 5 | BONDING_MASTER=yes 6 | BONDING_SLAVE_0=eth0 7 | BONDING_SLAVE_1=eth1 8 | MTU=1500 9 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/loopback: -------------------------------------------------------------------------------- 1 | # This file describes the network interfaces available on your system 2 | # and how to activate them. For more information, see interfaces(5). 3 | 4 | # The loopback network interface 5 | auto lo 6 | iface lo inet loopback 7 | mtu 65536 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_route/sles/advanced_routes: -------------------------------------------------------------------------------- 1 | 172.28.45.0/30 172.18.6.2 - eth0 table 200 2 | 172.17.67.0/30 172.18.6.2 - vlan200 table 200 3 | 10.10.10.0/30 172.18.6.2 - eth0 table 200 4 | default 10.0.0.1 - eth1 table 200 5 | 2a01:4f8:211:9d5:53::/96 2a01:4f8:211:9d5::2 - vlan200 table 200 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-bond1: -------------------------------------------------------------------------------- 1 | DEVICE=bond1 2 | ONBOOT=yes 3 | IPADDR=172.20.1.9 4 | NETMASK=255.255.255.0 5 | #GATEWAY=172.24.61.1 6 | #NO_ALIASROUTING=yes 7 | BONDING_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 8 | #USER_CTL=no 9 | MTU=1500 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-bond1~: -------------------------------------------------------------------------------- 1 | DEVICE=bond1 2 | ONBOOT=yes 3 | IPADDR=172.20.1.9 4 | NETMASK=255.255.255.0 5 | #GATEWAY=172.24.61.1 6 | #NO_ALIASROUTING=yes 7 | BONDING_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 8 | #USER_CTL=no 9 | MTU=1500 10 | -------------------------------------------------------------------------------- /spec/spec_helper_methods.rb: -------------------------------------------------------------------------------- 1 | PROJECT_ROOT = File.expand_path('..', File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(PROJECT_ROOT, 'lib')) 3 | fixture_path = File.expand_path(File.join('spec', 'fixtures'), PROJECT_ROOT) 4 | $LOAD_PATH.concat(Dir.glob(File.join(fixture_path, 'modules', '*', 'lib'))) 5 | -------------------------------------------------------------------------------- /.fixtures.yml: -------------------------------------------------------------------------------- 1 | --- 2 | fixtures: 3 | repositories: 4 | augeas_core: https://github.com/puppetlabs/puppetlabs-augeas_core 5 | filemapper: https://github.com/voxpupuli/puppet-filemapper 6 | kmod: https://github.com/voxpupuli/puppet-kmod 7 | stdlib: https://github.com/puppetlabs/puppetlabs-stdlib 8 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/network-scripts/ifcfg-bond0: -------------------------------------------------------------------------------- 1 | DEVICE=bond0 2 | ONBOOT=yes 3 | #IPADDR=172.24.61.11 4 | #NETMASK=255.255.255.0 5 | #GATEWAY=172.24.61.1 6 | #NO_ALIASROUTING=yes 7 | BONDING_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 8 | #USER_CTL=no 9 | MTU=1500 10 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/sles_spec/network-scripts/ifcfg-bond1~: -------------------------------------------------------------------------------- 1 | DEVICE=bond1 2 | STARTMODE=auto 3 | IPADDR=172.20.1.9 4 | NETMASK=255.255.255.0 5 | #GATEWAY=172.24.61.1 6 | BONDING_MODULE_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3+4" 7 | BONDING_MASTER=yes 8 | #USER_CTL=no 9 | MTU=1500 10 | -------------------------------------------------------------------------------- /spec/travis_rspec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | rubylib = [] 4 | 5 | modulepath = `bundle exec puppet config print modulepath` 6 | 7 | modulepath.split(':').each { |path| rubylib += Dir.glob("#{path}/*/lib") } 8 | 9 | ENV['RUBYLIB'] = rubylib.join(':') 10 | 11 | Kernel.exec "bundle exec rspec #{ARGV.join(' ')}" 12 | -------------------------------------------------------------------------------- /spec/classes/bond/setup_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'network::bond::setup', type: :class do 4 | describe 'on Debian' do 5 | let(:facts) do 6 | { 7 | os: { family: 'Debian' } 8 | } 9 | end 10 | 11 | it { is_expected.to contain_package('ifenslave') } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_route/routes_spec/advanced_routes: -------------------------------------------------------------------------------- 1 | 172.28.45.0 255.255.255.0 172.18.6.2 vlan200 table 200 2 | 172.17.67.0 255.255.255.0 172.18.6.2 vlan200 table 200 3 | 10.10.10.0 255.255.255.0 172.18.6.2 vlan200 table 200 4 | 2a01:4f8:211:9d5:53:: ffff:ffff:ffff:ffff:ffff:ffff:: 2a01:4f8:211:9d5::2 vlan200 table 200 5 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_route/routes_spec/simple_routes: -------------------------------------------------------------------------------- 1 | 172.28.45.0 255.255.255.0 172.18.6.2 vlan200 2 | 172.17.67.0 255.255.255.0 172.18.6.2 vlan200 3 | 10.10.10.0 255.255.255.0 172.18.6.2 vlan200 4 | 2a01:4f8:211:9d5:53:: ffff:ffff:ffff:ffff:ffff:ffff:: 2a01:4f8:211:9d5::2 vlan200 5 | default 0.0.0.0 172.18.6.2 vlan200 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_route/redhat/advanced_routes: -------------------------------------------------------------------------------- 1 | 172.28.45.0/30 via 172.18.6.2 dev eth0 table 200 2 | 172.17.67.0/30 via 172.18.6.2 dev vlan200 table 200 3 | 10.10.10.0/30 via 172.18.6.2 dev eth0 table 200 4 | default via 10.0.0.1 dev eth1 table 200 5 | 2a01:4f8:211:9d5:53::/96 via 2a01:4f8:211:9d5::2 dev vlan200 table 200 6 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/redhat_spec/lo: -------------------------------------------------------------------------------- 1 | DEVICE=lo 2 | IPADDR=127.0.0.1 3 | NETMASK=255.0.0.0 4 | NETWORK=127.0.0.0 5 | # If you're having problems with gated making 127.0.0.0/8 a martian, 6 | # you can change this to something else (255.255.255.255, for example) 7 | BROADCAST=127.255.255.255 8 | ONBOOT=yes 9 | NAME=loopback 10 | -------------------------------------------------------------------------------- /templates/bond/opts-redhat.erb: -------------------------------------------------------------------------------- 1 | <%= %w[mode miimon downdelay updelay lacp_rate primary primary_reselect xmit_hash_policy].reject { 2 | |option| scope.lookupvar(option).nil? || scope.lookupvar(option) == :undef || scope.lookupvar(option) == :absent || scope.lookupvar(option).empty? }.map { 3 | |key| "#{key}=#{scope.lookupvar(key)}" 4 | }.join(' ') -%> 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | # Managed by modulesync - DO NOT EDIT 4 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 5 | 6 | root = true 7 | 8 | [*] 9 | charset = utf-8 10 | end_of_line = lf 11 | indent_size = 2 12 | tab_width = 2 13 | indent_style = space 14 | insert_final_newline = true 15 | trim_trailing_whitespace = true 16 | -------------------------------------------------------------------------------- /spec/spec_helper_acceptance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Managed by modulesync - DO NOT EDIT 4 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 5 | 6 | require 'voxpupuli/acceptance/spec_helper_acceptance' 7 | 8 | configure_beaker(modules: :metadata) 9 | 10 | Dir['./spec/support/acceptance/**/*.rb'].sort.each { |f| require f } 11 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/single_interface_dhcp: -------------------------------------------------------------------------------- 1 | # This file describes the network interfaces available on your system 2 | # and how to activate them. For more information, see interfaces(5). 3 | 4 | # The loopback network interface 5 | auto lo 6 | iface lo inet loopback 7 | 8 | # The primary network interface 9 | allow-hotplug eth0 10 | iface eth0 inet dhcp 11 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/iface_whitespace: -------------------------------------------------------------------------------- 1 | # This file describes the network interfaces available on your system 2 | # and how to activate them. For more information, see interfaces(5). 3 | 4 | # The loopback network interface 5 | auto lo 6 | iface lo inet loopback 7 | 8 | # The primary network interface 9 | allow-hotplug eth0 10 | iface eth0 inet dhcp 11 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/two_interface_dhcp: -------------------------------------------------------------------------------- 1 | # This file describes the network interfaces available on your system 2 | # and how to activate them. For more information, see interfaces(5). 3 | 4 | # The loopback network interface 5 | auto lo 6 | iface lo inet loopback 7 | 8 | # The primary network interface 9 | allow-hotplug eth0 10 | iface eth0 inet dhcp 11 | 12 | allow-auto eth1 13 | iface eth1 inet dhcp 14 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/single_interface_options: -------------------------------------------------------------------------------- 1 | # This file describes the network interfaces available on your system 2 | # and how to activate them. For more information, see interfaces(5). 3 | 4 | # The loopback network interface 5 | auto lo 6 | iface lo inet loopback 7 | 8 | iface eth0 inet dhcp 9 | pre-up /bin/touch /tmp/eth0-up 10 | post-down /bin/touch /tmp/eth0-down1 11 | post-down /bin/touch /tmp/eth0-down2 12 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/jessie_source_stanza: -------------------------------------------------------------------------------- 1 | # This file describes the network interfaces available on your system 2 | # and how to activate them. For more information, see interfaces(5). 3 | 4 | source /etc/network/interfaces.d/* 5 | source-directory /etc/network/custom-ifaces 6 | 7 | # The loopback network interface 8 | auto lo 9 | iface lo inet loopback 10 | 11 | # The primary network interface 12 | allow-hotplug eth0 13 | iface eth0 inet dhcp 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | /pkg/ 5 | /Gemfile.lock 6 | /Gemfile.local 7 | /vendor/ 8 | /.vendor/ 9 | /spec/fixtures/manifests/ 10 | /spec/fixtures/modules/ 11 | /.vagrant/ 12 | /.bundle/ 13 | /.ruby-version 14 | /coverage/ 15 | /log/ 16 | /.idea/ 17 | /.dependencies/ 18 | /.librarian/ 19 | /Puppetfile.lock 20 | *.iml 21 | .*.sw? 22 | /.yardoc/ 23 | /Guardfile 24 | bolt-debug.log 25 | .rerun.json 26 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/single_interface_static: -------------------------------------------------------------------------------- 1 | # This file describes the network interfaces available on your system 2 | # and how to activate them. For more information, see interfaces(5). 3 | 4 | # The loopback network interface 5 | auto lo 6 | iface lo inet loopback 7 | 8 | # The primary network interface 9 | auto eth0 10 | iface eth0 inet static 11 | address 192.168.0.2 12 | broadcast 192.168.0.255 13 | netmask 255.255.255.0 14 | gateway 192.168.0.1 15 | mtu 1500 16 | -------------------------------------------------------------------------------- /spec/functions/compact_hash_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'compact_hash' do 4 | it 'returns an empty hash when given an empty hash' do 5 | is_expected.to run.with_params({}).and_return({}) 6 | end 7 | 8 | it 'returns a compacted hash with nil values removed' do 9 | is_expected.to run. 10 | with_params('key1' => 'value1', 'key2' => nil, 'key3' => '', 'key4' => false, 'key5' => true). 11 | and_return('key1' => 'value1', 'key3' => '', 'key4' => false, 'key5' => true) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | name: "Pull Request Labeler" 6 | 7 | # yamllint disable-line rule:truthy 8 | on: 9 | pull_request_target: {} 10 | 11 | permissions: 12 | contents: read 13 | pull-requests: write 14 | 15 | jobs: 16 | labeler: 17 | permissions: 18 | contents: read 19 | pull-requests: write 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/labeler@v5 23 | -------------------------------------------------------------------------------- /spec/functions/network/compact_hash_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'network::compact_hash' do 4 | it 'returns an empty hash when given an empty hash' do 5 | is_expected.to run.with_params({}).and_return({}) 6 | end 7 | 8 | it 'returns a compacted hash with nil values removed' do 9 | is_expected.to run. 10 | with_params('key1' => 'value1', 'key2' => nil, 'key3' => '', 'key4' => false, 'key5' => true). 11 | and_return('key1' => 'value1', 'key3' => '', 'key4' => false, 'key5' => true) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/flush_lo_eth0_dhcp: -------------------------------------------------------------------------------- 1 | # HEADER: /etc/network/interfaces is being managed by puppet. Changes to 2 | # HEADER: interfaces that are not being managed by puppet will persist; 3 | # HEADER: however changes to interfaces that are being managed by puppet will 4 | # HEADER: be overwritten. In addition, file order is NOT guaranteed. 5 | # HEADER: Last generated at: Thu Jan 19 16:23:31 -0800 2012 6 | 7 | 8 | auto lo 9 | 10 | allow-hotplug eth0 11 | 12 | iface eth0 inet dhcp 13 | 14 | iface lo inet loopback 15 | -------------------------------------------------------------------------------- /spec/unit/facter/network_primary_ip_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'facter' 5 | require 'facter/network' 6 | 7 | describe 'network_primary_ip' do 8 | subject(:fact) { Facter.fact(:network_primary_ip) } 9 | 10 | before do 11 | Facter.clear 12 | allow(Facter.fact(:networking)).to receive(:value).and_return({ 'ip' => '192.168.178.3' }) 13 | end 14 | 15 | it 'uses the built-in facts to resolve the primary ip address' do 16 | expect(fact.value).to eq('192.168.178.3') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | name: CI 6 | 7 | # yamllint disable-line rule:truthy 8 | on: 9 | pull_request: {} 10 | push: 11 | branches: 12 | - main 13 | - master 14 | 15 | concurrency: 16 | group: ${{ github.ref_name }} 17 | cancel-in-progress: true 18 | 19 | permissions: 20 | contents: read 21 | 22 | jobs: 23 | puppet: 24 | name: Puppet 25 | uses: voxpupuli/gha-puppet/.github/workflows/beaker.yml@v4 26 | -------------------------------------------------------------------------------- /spec/unit/facter/network_primary_interface_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'facter' 5 | require 'facter/network' 6 | 7 | describe 'network_primary_interface' do 8 | subject(:fact) { Facter.fact(:network_primary_interface) } 9 | 10 | before do 11 | Facter.clear 12 | allow(Facter.fact(:networking)).to receive(:value).and_return({ 'primary' => 'eth1' }) 13 | end 14 | 15 | it 'uses the built-in facts to determine the primary interface' do 16 | expect(fact.value).to eq('eth1') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /functions/compact_hash.pp: -------------------------------------------------------------------------------- 1 | # @summary Compresses a hash by removing all elements whose values are `undef` 2 | # 3 | # @param hash The hash to compact 4 | # @return A new hash with all `undef` values removed 5 | # @example 6 | # $example = { 7 | # 'one' => 'two', 8 | # 'red' => undef, 9 | # } 10 | # 11 | # network::compact_hash($example) 12 | # # => { 'one => 'two' } 13 | function network::compact_hash(Hash $hash) >> Hash { 14 | # Use the built-in `filter` function to remove all elements with `undef` values 15 | $hash.filter |$key, $value| { $value != undef } 16 | } 17 | -------------------------------------------------------------------------------- /spec/acceptance/network_route_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper_acceptance' 4 | 5 | describe 'network_route' do 6 | it 'creates a route' do 7 | pp = <<-EOS 8 | include network 9 | if $facts['os']['family'] == 'RedHat' { 10 | package{ 'network-scripts': ensure => 'present'} 11 | } 12 | network_route { '10.0.0.1': 13 | ensure => 'present', 14 | network => 'local', 15 | interface => 'lo', 16 | } 17 | EOS 18 | # Run it twice and test for idempotency 19 | apply_manifest(pp, catch_failures: true) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | #### Pull Request (PR) description 10 | 13 | 14 | #### This Pull Request (PR) fixes the following issues 15 | 21 | -------------------------------------------------------------------------------- /manifests/bond/setup.pp: -------------------------------------------------------------------------------- 1 | # @summary Setup bonding support for different operating systems 2 | # 3 | # This class installs the necessary packages for network bonding support. 4 | # On Debian systems, it installs the ifenslave package. On RedHat systems, 5 | # the ifenslave command is available by default with the iputils package. 6 | # 7 | class network::bond::setup { 8 | case $facts['os']['family'] { 9 | 'Debian': { 10 | package { 'ifenslave': 11 | ensure => present, 12 | } 13 | } 14 | 'RedHat', default: { 15 | # Redhat installs the ifenslave command with the iputils package which 16 | # is available by default 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/two_interfaces_static_vlan: -------------------------------------------------------------------------------- 1 | # This file describes the network interfaces available on your system 2 | # and how to activate them. For more information, see interfaces(5). 3 | 4 | # The loopback network interface 5 | auto lo 6 | iface lo inet loopback 7 | 8 | # The primary network interface 9 | auto eth0 10 | iface eth0 inet static 11 | address 192.168.0.2 12 | broadcast 192.168.0.255 13 | netmask 255.255.255.0 14 | gateway 192.168.0.1 15 | mtu 1500 16 | auto eth0.1 17 | iface eth0.1 inet static 18 | vlan-raw-device eth0 19 | address 172.16.0.2 20 | broadcast 172.16.0.255 21 | netmask 255.255.255.0 22 | gateway 172.16.0.1 23 | mtu 1500 24 | -------------------------------------------------------------------------------- /.pmtignore: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | /docs/ 5 | /pkg/ 6 | /Gemfile 7 | /Gemfile.lock 8 | /Gemfile.local 9 | /vendor/ 10 | /.vendor/ 11 | /spec/ 12 | /Rakefile 13 | /.vagrant/ 14 | /.bundle/ 15 | /.ruby-version 16 | /coverage/ 17 | /log/ 18 | /.idea/ 19 | /.dependencies/ 20 | /.github/ 21 | /.librarian/ 22 | /Puppetfile.lock 23 | /Puppetfile 24 | *.iml 25 | /.editorconfig 26 | /.fixtures.yml 27 | /.gitignore 28 | /.msync.yml 29 | /.overcommit.yml 30 | /.pmtignore 31 | /.rspec 32 | /.rspec_parallel 33 | /.rubocop.yml 34 | /.sync.yml 35 | .*.sw? 36 | /.yardoc/ 37 | /.yardopts 38 | /Dockerfile 39 | /HISTORY.md 40 | -------------------------------------------------------------------------------- /spec/fixtures/provider/network_config/interfaces_spec/three_interface_static: -------------------------------------------------------------------------------- 1 | # HEADER: This file is being managed by puppet. Changes to 2 | # HEADER: interfaces that are not being managed by puppet will persist; 3 | # HEADER: however changes to interfaces that are being managed by puppet will 4 | # HEADER: be overwritten. In addition, file order is NOT guaranteed. 5 | # HEADER: Last generated at: Tue Nov 27 01:11:36 +0200 2012 6 | 7 | 8 | # The following interfaces will be started on boot 9 | auto eth0 eth1 lo 10 | 11 | iface eth0 inet dhcp 12 | allow-hotplug true 13 | pre-up sleep 2 14 | 15 | iface eth1 inet static 16 | netmask 255.255.255.0 17 | address 192.168.50.2 18 | mtu 1500 19 | 20 | iface lo inet loopback 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | name: Release 6 | 7 | # yamllint disable-line rule:truthy 8 | on: 9 | push: 10 | tags: 11 | - '*' 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | release: 18 | name: Release 19 | uses: voxpupuli/gha-puppet/.github/workflows/release.yml@v3 20 | with: 21 | allowed_owner: 'voxpupuli' 22 | secrets: 23 | # Configure secrets here: 24 | # https://docs.github.com/en/actions/security-guides/encrypted-secrets 25 | username: ${{ secrets.PUPPET_FORGE_USERNAME }} 26 | api_key: ${{ secrets.PUPPET_FORGE_API_KEY }} 27 | -------------------------------------------------------------------------------- /lib/facter/network.rb: -------------------------------------------------------------------------------- 1 | require 'facter' 2 | 3 | Facter.add(:network_nexthop_ip) do 4 | confine kernel: 'Linux' 5 | confine { Facter::Util::Resolution.which('ip') } 6 | my_gw = nil 7 | setcode do 8 | gw_address = Facter::Util::Resolution.exec('ip route show 0/0') 9 | my_gw = gw_address.split(%r{\s+})[2].match(%r{^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$}).to_s if gw_address.include? ' via ' 10 | my_gw 11 | end 12 | end 13 | 14 | Facter.add(:network_primary_interface) do 15 | confine kernel: 'Linux' 16 | 17 | setcode do 18 | Facter.value(:networking)['primary'] 19 | end 20 | end 21 | 22 | Facter.add(:network_primary_ip) do 23 | confine kernel: 'Linux' 24 | 25 | setcode do 26 | Facter.value(:networking)['ip'] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## Affected Puppet, Ruby, OS and module versions/distributions 12 | 13 | - Puppet: 14 | - Ruby: 15 | - Distribution: 16 | - Module version: 17 | 18 | ## How to reproduce (e.g Puppet code you use) 19 | 20 | ## What are you seeing 21 | 22 | ## What behaviour did you expect instead 23 | 24 | ## Output log 25 | 26 | ## Any additional information you'd like to impart 27 | -------------------------------------------------------------------------------- /spec/unit/facter/network_nexthop_ip_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'facter' 5 | require 'facter/network' 6 | 7 | describe 'network_nexthop_ip fact' do 8 | subject(:fact) { Facter.fact(:network_nexthop_ip) } 9 | 10 | before do 11 | # perform any action that should be run before every test 12 | Facter.clear 13 | allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') 14 | allow(Facter::Util::Resolution).to receive(:which).with('ip').and_return('/usr/bin/ip') 15 | end 16 | 17 | it 'returns a the gateway' do 18 | expect(Facter::Util::Resolution).to receive(:exec).with('ip route show 0/0').and_return('default via 192.168.178.1 dev eth0') # rubocop:disable RSpec/MessageSpies 19 | expect(fact.value).to eq('192.168.178.1') 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.github/workflows/prepare_release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | name: 'Prepare Release' 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | version: 11 | description: 'Module version to be released. Must be a valid semver string without leading v. (1.2.3)' 12 | required: false 13 | 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | jobs: 19 | release_prep: 20 | uses: 'voxpupuli/gha-puppet/.github/workflows/prepare_release.yml@v3' 21 | with: 22 | version: ${{ github.event.inputs.version }} 23 | allowed_owner: 'voxpupuli' 24 | secrets: 25 | # Configure secrets here: 26 | # https://docs.github.com/en/actions/security-guides/encrypted-secrets 27 | github_pat: '${{ secrets.PCCI_PAT_RELEASE_PREP }}' 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Managed by modulesync - DO NOT EDIT 4 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 5 | 6 | # puppetlabs_spec_helper will set up coverage if the env variable is set. 7 | # We want to do this if lib exists and it hasn't been explicitly set. 8 | ENV['COVERAGE'] ||= 'yes' if Dir.exist?(File.expand_path('../lib', __dir__)) 9 | 10 | require 'voxpupuli/test/spec_helper' 11 | 12 | RSpec.configure do |c| 13 | c.facterdb_string_keys = false 14 | c.mock_with :rspec 15 | end 16 | 17 | add_mocked_facts! 18 | 19 | if File.exist?(File.join(__dir__, 'default_module_facts.yml')) 20 | facts = YAML.safe_load(File.read(File.join(__dir__, 'default_module_facts.yml'))) 21 | facts&.each do |name, value| 22 | add_custom_fact name.to_sym, value 23 | end 24 | end 25 | 26 | require 'spec_helper_methods' 27 | Dir['./spec/support/spec/**/*.rb'].sort.each { |f| require f } 28 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | source ENV['GEM_SOURCE'] || 'https://rubygems.org' 5 | 6 | group :test do 7 | gem 'voxpupuli-test', '~> 13.0', :require => false 8 | gem 'puppet_metadata', '~> 5.0', :require => false 9 | gem 'ipaddress', :require => false 10 | gem 'rspec-its', :require => false 11 | end 12 | 13 | group :development do 14 | gem 'guard-rake', :require => false 15 | gem 'overcommit', '>= 0.39.1', :require => false 16 | end 17 | 18 | group :system_tests do 19 | gem 'voxpupuli-acceptance', '~> 4.0', :require => false 20 | end 21 | 22 | group :release do 23 | gem 'voxpupuli-release', '~> 5.0', :require => false 24 | end 25 | 26 | gem 'rake', :require => false 27 | 28 | gem 'openvox', ENV.fetch('OPENVOX_GEM_VERSION', [">= 7", "< 9"]), :require => false, :groups => [:test] 29 | 30 | # vim: syntax=ruby 31 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 6 | 7 | changelog: 8 | exclude: 9 | labels: 10 | - duplicate 11 | - invalid 12 | - modulesync 13 | - question 14 | - skip-changelog 15 | - wont-fix 16 | - wontfix 17 | 18 | categories: 19 | - title: Breaking Changes 🛠 20 | labels: 21 | - backwards-incompatible 22 | 23 | - title: New Features 🎉 24 | labels: 25 | - enhancement 26 | 27 | - title: Bug Fixes 🐛 28 | labels: 29 | - bug 30 | 31 | - title: Documentation Updates 📚 32 | labels: 33 | - documentation 34 | - docs 35 | 36 | - title: Dependency Updates ⬆️ 37 | labels: 38 | - dependencies 39 | 40 | - title: Other Changes 41 | labels: 42 | - "*" 43 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | begin 5 | require 'voxpupuli/test/rake' 6 | rescue LoadError 7 | # only available if gem group test is installed 8 | end 9 | 10 | begin 11 | require 'voxpupuli/acceptance/rake' 12 | rescue LoadError 13 | # only available if gem group acceptance is installed 14 | end 15 | 16 | begin 17 | require 'voxpupuli/release/rake_tasks' 18 | rescue LoadError 19 | # only available if gem group releases is installed 20 | else 21 | GCGConfig.user = 'voxpupuli' 22 | GCGConfig.project = 'puppet-network' 23 | end 24 | 25 | desc "Run main 'test' task and report merged results to coveralls" 26 | task test_with_coveralls: [:test] do 27 | if Dir.exist?(File.expand_path('../lib', __FILE__)) 28 | require 'coveralls/rake/task' 29 | Coveralls::RakeTask.new 30 | Rake::Task['coveralls:push'].invoke 31 | else 32 | puts 'Skipping reporting to coveralls. Module has no lib dir' 33 | end 34 | end 35 | 36 | # vim: syntax=ruby 37 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/compact_hash.rb: -------------------------------------------------------------------------------- 1 | # @summary Compresses a hash to remove all elements whose values are nil or undef 2 | # 3 | # @deprecated This function is deprecated. Use the network::compact_hash() function instead. 4 | # 5 | # This function compresses a hash to remove all elements whose values are nil or undef. 6 | # It has been deprecated in favor of the network::compact_hash() function which provides 7 | # the same functionality using modern Puppet function syntax. 8 | # 9 | # @param hash The hash to compress 10 | # @return [Hash] A new hash with nil and undef values removed 11 | # 12 | # @example Compressing a hash 13 | # $example = { 14 | # 'one' => 'two', 15 | # 'red' => undef, 16 | # 'blue' => nil, 17 | # } 18 | # 19 | # compact_hash($example) 20 | # # => { 'one' => 'two' } 21 | # 22 | Puppet::Parser::Functions.newfunction(:compact_hash, 23 | type: :rvalue, 24 | arity: 1, 25 | doc: <<-EOD) do |args| 26 | @deprecated This function is deprecated. Use the network::compact_hash() function instead. 27 | 28 | Compresses a hash to remove all elements whose values are nil or undef. 29 | EOD 30 | 31 | Puppet.deprecation_warning('compact_hash() is deprecated. Use network::compact_hash() instead.') 32 | 33 | # Call the new network::compact_hash function 34 | call_function('network::compact_hash', args) 35 | end 36 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | # 4 | # Hooks are only enabled if you take action. 5 | # 6 | # To enable the hooks run: 7 | # 8 | # ``` 9 | # bundle exec overcommit --install 10 | # # ensure .overcommit.yml does not harm to you and then 11 | # bundle exec overcommit --sign 12 | # ``` 13 | # 14 | # (it will manage the .git/hooks directory): 15 | # 16 | # Examples howto skip a test for a commit or push: 17 | # 18 | # ``` 19 | # SKIP=RuboCop git commit 20 | # SKIP=PuppetLint git commit 21 | # SKIP=RakeTask git push 22 | # ``` 23 | # 24 | # Don't invoke overcommit at all: 25 | # 26 | # ``` 27 | # OVERCOMMIT_DISABLE=1 git commit 28 | # ``` 29 | # 30 | # Read more about overcommit: https://github.com/brigade/overcommit 31 | # 32 | # To manage this config yourself in your module add 33 | # 34 | # ``` 35 | # .overcommit.yml: 36 | # unmanaged: true 37 | # ``` 38 | # 39 | # to your modules .sync.yml config 40 | --- 41 | PreCommit: 42 | RuboCop: 43 | enabled: true 44 | description: 'Runs rubocop on modified files only' 45 | command: ['bundle', 'exec', 'rubocop'] 46 | RakeTarget: 47 | enabled: true 48 | description: 'Runs lint on modified files only' 49 | targets: 50 | - 'lint' 51 | command: ['bundle', 'exec', 'rake'] 52 | YamlSyntax: 53 | enabled: true 54 | JsonSyntax: 55 | enabled: true 56 | TrailingWhitespace: 57 | enabled: true 58 | 59 | PrePush: 60 | RakeTarget: 61 | enabled: true 62 | description: 'Run rake targets' 63 | targets: 64 | - 'validate' 65 | - 'test' 66 | - 'rubocop' 67 | command: ['bundle', 'exec', 'rake'] 68 | -------------------------------------------------------------------------------- /manifests/init.pp: -------------------------------------------------------------------------------- 1 | # @summary Install the packages and gems required by the network_route and network_config resources 2 | # 3 | # @param ifupdown_extra 4 | # The name of the ifupdown-extra package 5 | # @param ifupdown_extra_provider 6 | # The provider of the ifupdown-extra package 7 | # @param manage_ifupdown_extra 8 | # Whether this class should manage the ifupdown-extra package 9 | # @param ensure_ifupdown_extra 10 | # What state the ifupdown-extra package should be in 11 | # @param ipaddress 12 | # The name of the ipaddress gems 13 | # @param ipaddress_provider 14 | # The provider of the ipaddress gem 15 | # @param manage_ipaddress 16 | # Whether this class should manage the ipaddress gem 17 | # @param ensure_ipaddress 18 | # What state the ipaddress package should be in 19 | # 20 | class network ( 21 | String[1] $ifupdown_extra = 'ifupdown-extra', 22 | Optional[String[1]] $ifupdown_extra_provider = undef, 23 | Boolean $manage_ifupdown_extra = true, 24 | Stdlib::Ensure::Package $ensure_ifupdown_extra = present, 25 | String[1] $ipaddress = 'ipaddress', 26 | String[1] $ipaddress_provider = 'puppet_gem', 27 | Boolean $manage_ipaddress = true, 28 | Stdlib::Ensure::Package $ensure_ipaddress = absent, 29 | ) { 30 | if $facts['os']['family'] == 'Debian' and $manage_ifupdown_extra { 31 | package { $ifupdown_extra: 32 | ensure => $ensure_ifupdown_extra, 33 | provider => $ifupdown_extra_provider, 34 | } 35 | Package[$ifupdown_extra] -> Network_route <| |> 36 | } 37 | 38 | if $manage_ipaddress { 39 | package { $ipaddress: 40 | ensure => $ensure_ipaddress, 41 | provider => $ipaddress_provider, 42 | } 43 | Package[$ipaddress] -> Network_config <| |> 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppet-network", 3 | "version": "2.2.2-rc0", 4 | "author": "Vox Pupuli", 5 | "license": "Apache-2.0", 6 | "summary": "Manage non-volatile network configuration", 7 | "source": "https://github.com/voxpupuli/puppet-network", 8 | "project_page": "https://github.com/voxpupuli/puppet-network", 9 | "issues_url": "https://github.com/voxpupuli/puppet-network/issues", 10 | "tags": [ 11 | "network", 12 | "voxpupuli" 13 | ], 14 | "operatingsystem_support": [ 15 | { 16 | "operatingsystem": "RedHat", 17 | "operatingsystemrelease": [ 18 | "7", 19 | "8" 20 | ] 21 | }, 22 | { 23 | "operatingsystem": "CentOS", 24 | "operatingsystemrelease": [ 25 | "7", 26 | "8" 27 | ] 28 | }, 29 | { 30 | "operatingsystem": "Rocky", 31 | "operatingsystemrelease": [ 32 | "8" 33 | ] 34 | }, 35 | { 36 | "operatingsystem": "AlmaLinux", 37 | "operatingsystemrelease": [ 38 | "8" 39 | ] 40 | }, 41 | { 42 | "operatingsystem": "OracleLinux", 43 | "operatingsystemrelease": [ 44 | "7", 45 | "8" 46 | ] 47 | }, 48 | { 49 | "operatingsystem": "Scientific", 50 | "operatingsystemrelease": [ 51 | "7" 52 | ] 53 | }, 54 | { 55 | "operatingsystem": "Debian", 56 | "operatingsystemrelease": [ 57 | "10", 58 | "11", 59 | "12" 60 | ] 61 | }, 62 | { 63 | "operatingsystem": "SLES", 64 | "operatingsystemrelease": [ 65 | "15" 66 | ] 67 | } 68 | ], 69 | "dependencies": [ 70 | { 71 | "name": "puppetlabs/stdlib", 72 | "version_requirement": ">= 4.13.1 < 10.0.0" 73 | }, 74 | { 75 | "name": "puppet/filemapper", 76 | "version_requirement": ">= 2.0.1 < 5.0.0" 77 | }, 78 | { 79 | "name": "puppet/kmod", 80 | "version_requirement": ">= 3.0.0 < 5.0.0" 81 | } 82 | ], 83 | "requirements": [ 84 | { 85 | "name": "openvox", 86 | "version_requirement": ">= 8.19.0 < 9.0.0" 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /spec/defines/bond_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'network::bond', type: :define do 4 | let(:title) { 'bond0' } 5 | 6 | let(:params) do 7 | { 8 | 'ensure' => 'present', 9 | 'method' => 'static', 10 | 'ipaddress' => '172.18.1.2', 11 | 'netmask' => '255.255.128.0', 12 | 'slaves' => %w[eth0 eth1], 13 | 'mtu' => 1550, 14 | 'options' => { 'NM_CONTROLLED' => 'yes' }, 15 | 'slave_options' => { 'NM_CONTROLLED' => 'no' }, 16 | 17 | 'mode' => 'active-backup', 18 | 'miimon' => '100', 19 | 'downdelay' => '200', 20 | 'updelay' => '200', 21 | 'lacp_rate' => 'slow', 22 | 'primary' => 'eth0', 23 | 'primary_reselect' => 'always', 24 | 'xmit_hash_policy' => 'layer2' 25 | } 26 | end 27 | 28 | describe 'on platform' do 29 | describe 'RedHat' do 30 | let(:facts) do 31 | { 32 | os: { family: 'RedHat' }, 33 | augeasversion: '1.4.0' 34 | } 35 | end 36 | 37 | it "creates 'network::bond::redhat'" do 38 | is_expected.to contain_network__bond__redhat('bond0') 39 | end 40 | 41 | it "forwards all options to 'network::bond::redhat'" do 42 | is_expected.to contain_network__bond__redhat('bond0').with(params) 43 | end 44 | end 45 | 46 | describe 'Debian' do 47 | let(:facts) do 48 | { 49 | os: { family: 'Debian' }, 50 | augeasversion: '1.4.0' 51 | } 52 | end 53 | 54 | it "creates 'network::bond::debian'" do 55 | is_expected.to contain_network__bond__debian('bond0') 56 | end 57 | 58 | it "forwards all options to 'network::bond::debian'" do 59 | is_expected.to contain_network__bond__debian('bond0').with(params) 60 | end 61 | end 62 | 63 | describe 'on an unsupported osfamily' do 64 | let(:facts) do 65 | { 66 | os: { family: 'SparrowOS' } 67 | } 68 | end 69 | 70 | it 'fails to compile' do 71 | is_expected.to compile.and_raise_error(%r{network::bond does not support osfamily 'SparrowOS'}) 72 | end 73 | end 74 | end 75 | 76 | describe 'configuring the kernel bonding device' do 77 | let(:facts) do 78 | { 79 | os: { family: 'Debian' }, 80 | augeasversion: '1.4.0' 81 | } 82 | end 83 | 84 | it { is_expected.to contain_class('network::bond::setup') } 85 | 86 | it 'adds a kernel module alias for the bonded device' do 87 | is_expected.to contain_kmod__alias('bond0').with(source: 'bonding', 88 | ensure: 'present') 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/puppet/type/network_route.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | require_relative '../../puppet_x/voxpupuli/utils' 3 | 4 | Puppet::Type.newtype(:network_route) do 5 | @doc = 'Manage non-volatile route configuration information' 6 | 7 | feature :provider_options, <<-EOD 8 | The provider can accept a hash of arbitrary options. The semantics of 9 | these options will depend on the provider. 10 | EOD 11 | 12 | include PuppetX::Voxpupuli::Utils 13 | 14 | ensurable 15 | 16 | newparam(:name) do 17 | isnamevar 18 | desc 'The name of the network route' 19 | end 20 | 21 | newproperty(:network) do 22 | isrequired 23 | desc 'The target network address' 24 | validate do |value| 25 | unless %w[default local].include?(value) 26 | a = PuppetX::Voxpupuli::Utils.try { IPAddr.new(value) } 27 | raise("Invalid value for parameter 'network': #{value}") unless a 28 | end 29 | end 30 | end 31 | 32 | newproperty(:netmask) do 33 | desc 'The subnet mask to apply to the route' 34 | 35 | validate do |value| 36 | raise("Invalid value for parameter 'netmask': #{value}") unless value.length <= 3 || PuppetX::Voxpupuli::Utils.try { IPAddr.new(value) } 37 | end 38 | 39 | munge do |value| 40 | # '255.255.255.255'.to_i will return 255, so we try to convert it back: 41 | if value.to_i.to_s == value 42 | # what are the chances someone is using /16 for their IPv6 network? 43 | addr = value.to_i <= 32 ? '255.255.255.255' : 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' 44 | IPAddr.new(addr).mask(value.strip.to_i).to_s 45 | elsif PuppetX::Voxpupuli::Utils.try { IPAddr.new(value).ipv6? } 46 | IPAddr.new('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff').mask(value).to_s 47 | elsif PuppetX::Voxpupuli::Utils.try { IPAddr.new(value).ipv4? } 48 | IPAddr.new('255.255.255.255').mask(value).to_s 49 | else 50 | raise("Invalid value for parameter 'netmask': #{value}") 51 | end 52 | end 53 | end 54 | 55 | newproperty(:gateway) do 56 | desc 'The gateway to use for the route' 57 | 58 | validate do |value| 59 | IPAddr.new(value) 60 | rescue ArgumentError 61 | raise("Invalid value for parameter 'gateway': #{value}") 62 | end 63 | end 64 | 65 | newproperty(:interface) do 66 | isrequired 67 | desc 'The interface to use for the route' 68 | end 69 | 70 | # `:options` provides an arbitrary passthrough for provider properties, so 71 | # that provider specific behavior doesn't clutter up the main type but still 72 | # allows for more powerful actions to be taken. 73 | newproperty(:options, required_features: :provider_options) do 74 | desc 'Provider specific options to be passed to the provider' 75 | 76 | validate do |value| 77 | raise ArgumentError, "#{self.class} requires a string for the options property" unless value.is_a?(String) 78 | end 79 | end 80 | 81 | validate do 82 | raise "Network_route[#{self[:name]}] must have netmask defined" if self[:network] != 'local' && self[:netmask].nil? 83 | raise "Network_route[#{self[:name]}] must have gateway defined" if self[:network] != 'local' && self[:gateway].nil? 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2023-08-17 21:35:26 UTC using RuboCop version 1.50.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 8 10 | # Configuration parameters: AllowedMethods. 11 | # AllowedMethods: enums 12 | Lint/ConstantDefinitionInBlock: 13 | Exclude: 14 | - 'lib/puppet/provider/network_config/interfaces.rb' 15 | - 'lib/puppet/provider/network_config/redhat.rb' 16 | - 'lib/puppet/provider/network_route/routes.rb' 17 | 18 | # Offense count: 2 19 | # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. 20 | Lint/DuplicateBranch: 21 | Exclude: 22 | - 'lib/puppet/provider/network_config/interfaces.rb' 23 | 24 | # Offense count: 1 25 | # Configuration parameters: AllowComments, AllowEmptyLambdas. 26 | Lint/EmptyBlock: 27 | Exclude: 28 | - 'lib/puppet/type/network_config.rb' 29 | 30 | # Offense count: 21 31 | # Configuration parameters: Max. 32 | RSpec/IndexedLet: 33 | Exclude: 34 | - 'spec/unit/provider/network_config/interfaces_spec.rb' 35 | - 'spec/unit/provider/network_config/redhat_spec.rb' 36 | - 'spec/unit/provider/network_config/sles_spec.rb' 37 | - 'spec/unit/provider/network_route/redhat_spec.rb' 38 | - 'spec/unit/provider/network_route/routes_spec.rb' 39 | - 'spec/unit/provider/network_route/sles_spec.rb' 40 | - 'spec/unit/type/network_config_spec.rb' 41 | 42 | # Offense count: 20 43 | # Configuration parameters: AllowSubject. 44 | RSpec/MultipleMemoizedHelpers: 45 | Max: 7 46 | 47 | # Offense count: 1 48 | RSpec/StubbedMock: 49 | Exclude: 50 | - 'spec/unit/facter/network_nexthop_ip_spec.rb' 51 | 52 | # Offense count: 30 53 | # This cop supports unsafe autocorrection (--autocorrect-all). 54 | # Configuration parameters: . 55 | # SupportedStyles: constant, string 56 | RSpec/VerifiedDoubleReference: 57 | EnforcedStyle: string 58 | 59 | # Offense count: 25 60 | # This cop supports unsafe autocorrection (--autocorrect-all). 61 | # Configuration parameters: EnforcedStyle. 62 | # SupportedStyles: always, always_true, never 63 | Style/FrozenStringLiteralComment: 64 | Enabled: false 65 | 66 | # Offense count: 1 67 | # This cop supports unsafe autocorrection (--autocorrect-all). 68 | # Configuration parameters: AllowedReceivers. 69 | # AllowedReceivers: Thread.current 70 | Style/HashEachMethods: 71 | Exclude: 72 | - 'lib/puppet/provider/network_config/redhat.rb' 73 | 74 | # Offense count: 3 75 | # This cop supports unsafe autocorrection (--autocorrect-all). 76 | # Configuration parameters: EnforcedStyle. 77 | # SupportedStyles: literals, strict 78 | Style/MutableConstant: 79 | Exclude: 80 | - 'lib/puppet/provider/network_config/redhat.rb' 81 | 82 | # Offense count: 5 83 | # This cop supports unsafe autocorrection (--autocorrect-all). 84 | # Configuration parameters: Methods. 85 | Style/RedundantArgument: 86 | Exclude: 87 | - 'lib/puppet/provider/network_config/interfaces.rb' 88 | - 'spec/unit/provider/network_route/redhat_spec.rb' 89 | - 'spec/unit/provider/network_route/routes_spec.rb' 90 | 91 | # Offense count: 1 92 | # This cop supports unsafe autocorrection (--autocorrect-all). 93 | # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. 94 | # AllowedMethods: present?, blank?, presence, try, try! 95 | Style/SafeNavigation: 96 | Exclude: 97 | - 'lib/puppet/provider/network_config/interfaces.rb' 98 | 99 | # Offense count: 3 100 | # This cop supports unsafe autocorrection (--autocorrect-all). 101 | # Configuration parameters: Mode. 102 | Style/StringConcatenation: 103 | Exclude: 104 | - 'lib/puppet/provider/network_config/interfaces.rb' 105 | -------------------------------------------------------------------------------- /lib/puppet/provider/network_route/sles.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | require 'puppetx/filemapper' 3 | 4 | Puppet::Type.type(:network_route).provide(:sles) do 5 | # SLES network_route routes provider. 6 | # 7 | # This provider uses the filemapper mixin to map the routes file to a 8 | # collection of network_route providers, and back. 9 | # 10 | # @see https://documentation.suse.com/sles/15-SP4/html/SLES-all/cha-network.html#sec-network-manconf-files-routes 11 | 12 | include PuppetX::FileMapper 13 | 14 | desc 'SLES style routes provider' 15 | 16 | confine 'os.family' => :suse 17 | defaultfor 'os.family' => :suse 18 | 19 | has_feature :provider_options 20 | 21 | def select_file 22 | "/etc/sysconfig/network/ifroute-#{interface}" 23 | end 24 | 25 | def self.target_files(script_dir = '/etc/sysconfig/network') 26 | entries = Dir.entries(script_dir).select { |entry| entry.match %r{ifroute-.*} } 27 | entries.map { |entry| File.join(script_dir, entry) } 28 | end 29 | 30 | def self.parse_file(_filename, contents) 31 | routes = [] 32 | lines = contents.split("\n") 33 | lines.each do |line| 34 | # Strip off any trailing comments 35 | line.sub!(%r{#.*$}, '') 36 | 37 | if line =~ %r{^\s*#|^\s*$} 38 | # Ignore comments and blank lines 39 | next 40 | end 41 | 42 | route = line.split(' ', 5) 43 | raise Puppet::Error, 'Malformed SLES route file, cannot instantiate network_route resources' if route.length < 4 44 | 45 | new_route = {} 46 | 47 | new_route[:gateway] = route[1] 48 | new_route[:interface] = route[3] 49 | new_route[:options] = route[4] if route[4] 50 | 51 | if ['default', '0.0.0.0'].include? route[0] 52 | new_route[:name] = 'default' # FIXME: Must match :name in order for changes to be detected 53 | new_route[:network] = 'default' 54 | new_route[:netmask] = '0.0.0.0' 55 | else 56 | ip = IPAddr.new(route[0]) 57 | netmask_addr = ip.prefix <= 32 ? '255.255.255.255' : 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' 58 | netmask = IPAddr.new("#{netmask_addr}/#{ip.prefix}") 59 | new_route[:name] = "#{ip}/#{ip.prefix}" # FIXME: Must match :name in order for changes to be detected 60 | new_route[:network] = ip.to_s 61 | new_route[:netmask] = netmask.to_s 62 | end 63 | routes << new_route 64 | end 65 | 66 | routes 67 | end 68 | 69 | # Generate an array of sections 70 | def self.format_file(_filename, providers) 71 | contents = [] 72 | contents << header 73 | # Build routes 74 | providers.sort_by(&:name).each do |provider| 75 | %w[network netmask gateway interface].each do |prop| 76 | raise Puppet::Error, "#{provider.name} is missing the required parameter '#{prop}'." if provider.send(prop).nil? 77 | end 78 | contents << if provider.network == 'default' 79 | "#{provider.network} #{provider.gateway} - #{provider.interface}" 80 | else 81 | ip = IPAddr.new("#{provider.network}/#{provider.netmask}") 82 | "#{ip}/#{ip.prefix} #{provider.gateway} - #{provider.interface}" 83 | end 84 | contents << (provider.options == :absent ? "\n" : " #{provider.options}\n") 85 | end 86 | contents.join 87 | end 88 | 89 | def self.header 90 | <<~HEADER 91 | # HEADER: This file is being managed by puppet. Changes to 92 | # HEADER: routes that are not being managed by puppet will persist; 93 | # HEADER: however changes to routes that are being managed by puppet will 94 | # HEADER: be overwritten. In addition, file order is NOT guaranteed. 95 | # HEADER: Last generated at: #{Time.now} 96 | HEADER 97 | end 98 | 99 | def self.post_flush_hook(filename) 100 | File.chmod(0o644, filename) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /manifests/bond/redhat.pp: -------------------------------------------------------------------------------- 1 | # @summary Instantiate bonded interfaces on Redhat based systems 2 | # 3 | # @param slaves 4 | # A list of slave interfaces to combine for the bonded interface 5 | # @param ensure 6 | # The ensure value for the bonding interface 7 | # @param ipaddress 8 | # The IPv4 address of the interface 9 | # @param netmask 10 | # The IPv4 network mask of the interface 11 | # @param method 12 | # The network configuration method 13 | # @param family 14 | # The IP family (inet or inet6) 15 | # @param onboot 16 | # Whether to bring the interface up on boot 17 | # @param hotplug 18 | # Whether to allow hotplug for the interface 19 | # @param mtu 20 | # The Maximum Transmission Unit size to use for bond interface and all slaves 21 | # @param options 22 | # Hash with custom interfaces options 23 | # @param slave_options 24 | # Hash with custom slave interfaces options 25 | # @param mode 26 | # The interface bond mode 27 | # @param miimon 28 | # The MII link monitoring frequency in milliseconds 29 | # @param downdelay 30 | # The time in milliseconds, to wait before disabling a slave after a link failure 31 | # @param updelay 32 | # The time in milliseconds to wait before enabling a slave after a link recovery 33 | # @param lacp_rate 34 | # LACPDU packet transmission rate in 802.3ad mode 35 | # @param primary 36 | # The primary interface to use when the mode is 'active-backup' 37 | # @param primary_reselect 38 | # Specifies the reselection policy for the primary slave 39 | # @param xmit_hash_policy 40 | # Selects the transmit hash policy to use for slave selection 41 | # 42 | # @see https://access.redhat.com/knowledge/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/sec-Using_Channel_Bonding.html Red Hat Deployment Guide 25.7.2 "Using Channel Bonding" 43 | # 44 | define network::bond::redhat ( 45 | Array[String[1]] $slaves, 46 | Stdlib::Ensure::Package $ensure = present, 47 | Optional[Stdlib::IP::Address::Nosubnet] $ipaddress = undef, 48 | Optional[String[1]] $netmask = undef, 49 | Optional[String[1]] $method = undef, 50 | Optional[Enum['inet', 'inet6']] $family = undef, 51 | Optional[Boolean] $onboot = undef, 52 | Optional[ 53 | Variant[Boolean, Enum['true', 'false']] 54 | ] $hotplug = undef, 55 | Optional[ 56 | Variant[Integer[42, 65536], Pattern[/^\d+$/]] 57 | ] $mtu = undef, 58 | Optional[Hash[String, Any]] $options = undef, 59 | Optional[Hash[String, Any]] $slave_options = undef, 60 | Optional[String[1]] $mode = undef, 61 | Optional[String[1]] $miimon = undef, 62 | Optional[String[1]] $downdelay = undef, 63 | Optional[String[1]] $updelay = undef, 64 | Optional[String[1]] $lacp_rate = undef, 65 | Optional[String[1]] $primary = undef, 66 | Optional[String[1]] $primary_reselect = undef, 67 | Optional[String[1]] $xmit_hash_policy = undef, 68 | ) { 69 | $opts = merge({ 'BONDING_OPTS' => template('network/bond/opts-redhat.erb'), }, 70 | $options 71 | ) 72 | 73 | network_config { $name: 74 | ensure => $ensure, 75 | method => $method, 76 | ipaddress => $ipaddress, 77 | netmask => $netmask, 78 | family => $family, 79 | onboot => $onboot, 80 | hotplug => $hotplug, 81 | mtu => $mtu, 82 | options => $opts, 83 | } 84 | 85 | $opts_slave = merge({ 86 | 'MASTER' => $name, 87 | 'SLAVE' => 'yes', 88 | }, 89 | $slave_options 90 | ) 91 | 92 | network_config { $slaves: 93 | ensure => $ensure, 94 | method => static, 95 | onboot => true, 96 | hotplug => false, 97 | options => $opts_slave, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/puppet/provider/network_route/routes.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | require 'puppetx/filemapper' 3 | 4 | Puppet::Type.type(:network_route).provide(:routes) do 5 | # Debian network_route routes provider. 6 | # 7 | # This provider uses the filemapper mixin to map the routes file to a 8 | # collection of network_route providers, and back. 9 | # 10 | # @see http://wiki.debian.org/NetworkConfiguration 11 | # @see http://packages.debian.org/squeeze/ifupdown-extras 12 | 13 | include PuppetX::FileMapper 14 | 15 | desc 'Debian routes style provider' 16 | 17 | confine 'os.family' => :debian 18 | 19 | # $ dpkg -S /etc/network/if-up.d/20static-routes 20 | # ifupdown-extra: /etc/network/if-up.d/20static-routes 21 | confine exists: '/etc/network/if-up.d/20static-routes' 22 | 23 | defaultfor 'os.family' => :debian 24 | 25 | has_feature :provider_options 26 | 27 | def select_file 28 | '/etc/network/routes' 29 | end 30 | 31 | def self.target_files 32 | ['/etc/network/routes'] 33 | end 34 | 35 | class MalformedRoutesError < Puppet::Error 36 | def initialize(msg = nil) 37 | msg = 'Malformed debian routes file; cannot instantiate network_route resources' if msg.nil? 38 | super 39 | end 40 | end 41 | 42 | def self.raise_malformed 43 | @failed = true 44 | raise MalformedRoutesError 45 | end 46 | 47 | def self.parse_file(_filename, contents) 48 | # Build out an empty hash for new routes for storing their configs. 49 | route_hash = Hash.new do |hash, key| 50 | hash[key] = {} 51 | hash[key][:name] = key 52 | hash[key] 53 | end 54 | 55 | lines = contents.split("\n") 56 | lines.each do |line| 57 | # Strip off any trailing comments 58 | line.sub!(%r{#.*$}, '') 59 | 60 | if line =~ %r{^\s*#|^\s*$} 61 | # Ignore comments and blank lines 62 | next 63 | end 64 | 65 | route = line.split(' ', 5) 66 | 67 | raise_malformed if route.length < 4 68 | 69 | if route[0] == 'default' 70 | name = 'default' 71 | route_hash[name][:network] = 'default' 72 | route_hash[name][:netmask] = '0.0.0.0' 73 | else 74 | # use the CIDR version of the target as :name 75 | name = "#{route[0]}/#{IPAddr.new(route[1]).to_i.to_s(2).count('1')}" 76 | route_hash[name][:network] = route[0] 77 | route_hash[name][:netmask] = route[1] 78 | end 79 | route_hash[name][:gateway] = route[2] 80 | route_hash[name][:interface] = route[3] 81 | route_hash[name][:options] = route[4] if route[4] 82 | end 83 | 84 | route_hash.values 85 | end 86 | 87 | # Generate an array of sections 88 | def self.format_file(_filename, providers) 89 | contents = [] 90 | contents << header 91 | 92 | # Build routes 93 | providers.sort_by(&:name).each do |provider| 94 | raise Puppet::Error, "#{provider.name} is missing the required parameter 'network'." if provider.network.nil? 95 | raise Puppet::Error, "#{provider.name} is missing the required parameter 'netmask'." if provider.netmask.nil? 96 | raise Puppet::Error, "#{provider.name} is missing the required parameter 'gateway'." if provider.gateway.nil? 97 | raise Puppet::Error, "#{provider.name} is missing the required parameter 'interface'." if provider.interface.nil? 98 | 99 | netmask = (provider.name == 'default' ? '0.0.0.0' : provider.netmask) 100 | 101 | contents << "#{provider.network} #{netmask} #{provider.gateway} #{provider.interface}" 102 | contents << (provider.options == :absent ? "\n" : " #{provider.options}\n") 103 | end 104 | 105 | contents.join 106 | end 107 | 108 | def self.header 109 | <<~HEADER 110 | # HEADER: This file is being managed by puppet. Changes to 111 | # HEADER: routes that are not being managed by puppet will persist; 112 | # HEADER: however changes to routes that are being managed by puppet will 113 | # HEADER: be overwritten. In addition, file order is NOT guaranteed. 114 | # HEADER: Last generated at: #{Time.now} 115 | HEADER 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /manifests/bond/debian.pp: -------------------------------------------------------------------------------- 1 | # @summary Instantiate bonded interfaces on Debian based systems 2 | # 3 | # @param slaves 4 | # A list of slave interfaces to combine for the bonded interface 5 | # @param ensure 6 | # The ensure value for the bonding interface 7 | # @param ipaddress 8 | # The IPv4 address of the interface 9 | # @param netmask 10 | # The IPv4 network mask of the interface 11 | # @param method 12 | # The network configuration method 13 | # @param family 14 | # The IP family (inet or inet6) 15 | # @param onboot 16 | # Whether to bring the interface up on boot 17 | # @param hotplug 18 | # Whether to allow hotplug for the interface 19 | # @param mtu 20 | # The Maximum Transmission Unit size to use for bond interface and all slaves 21 | # @param options 22 | # Hash with custom interfaces options 23 | # @param slave_options 24 | # Hash with custom slave interfaces options 25 | # @param mode 26 | # The interface bond mode 27 | # @param miimon 28 | # The MII link monitoring frequency in milliseconds 29 | # @param downdelay 30 | # The time in milliseconds, to wait before disabling a slave after a link failure 31 | # @param updelay 32 | # The time in milliseconds to wait before enabling a slave after a link recovery 33 | # @param lacp_rate 34 | # LACPDU packet transmission rate in 802.3ad mode 35 | # @param primary 36 | # The primary interface to use when the mode is 'active-backup' 37 | # @param primary_reselect 38 | # Specifies the reselection policy for the primary slave 39 | # @param xmit_hash_policy 40 | # Selects the transmit hash policy to use for slave selection 41 | # 42 | # @see https://wiki.debian.org/Bonding Debian Network Bonding 43 | # 44 | define network::bond::debian ( 45 | Array[String[1]] $slaves, 46 | Stdlib::Ensure::Package $ensure = present, 47 | Optional[Stdlib::IP::Address::Nosubnet] $ipaddress = undef, 48 | Optional[String[1]] $netmask = undef, 49 | Optional[String[1]] $method = undef, 50 | Optional[Enum['inet', 'inet6']] $family = undef, 51 | Optional[Boolean] $onboot = undef, 52 | Optional[ 53 | Variant[Boolean, Enum['true', 'false']] 54 | ] $hotplug = undef, 55 | Optional[ 56 | Variant[Integer[42, 65536], Pattern[/^\d+$/]] 57 | ] $mtu = undef, 58 | Optional[Hash[String, Any]] $options = undef, 59 | Optional[Hash[String, Any]] $slave_options = undef, 60 | Optional[String[1]] $mode = undef, 61 | Optional[String[1]] $miimon = undef, 62 | Optional[String[1]] $downdelay = undef, 63 | Optional[String[1]] $updelay = undef, 64 | Optional[String[1]] $lacp_rate = undef, 65 | Optional[String[1]] $primary = undef, 66 | Optional[String[1]] $primary_reselect = undef, 67 | Optional[String[1]] $xmit_hash_policy = undef, 68 | ) { 69 | $raw = { 70 | 'bond-slaves' => join($slaves, ' '), 71 | 'bond-mode' => $mode, 72 | 'bond-miimon' => $miimon, 73 | 'bond-downdelay' => $downdelay, 74 | 'bond-updelay' => $updelay, 75 | 'bond-lacp-rate' => $lacp_rate, 76 | 'bond-primary' => $primary, 77 | 'bond-primary-reselect' => $primary_reselect, 78 | 'bond-xmit-hash-policy' => $xmit_hash_policy, 79 | } 80 | 81 | if $mtu =~ String { 82 | warning('$mtu should be an integer and will change in future releases') 83 | } 84 | if $mtu { 85 | # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1224007 86 | $raw_post_up = { 'post-up' => "ip link set dev ${name} mtu ${mtu}", } 87 | } else { 88 | $raw_post_up = {} 89 | } 90 | 91 | $opts = network::compact_hash(merge($raw, $raw_post_up, $options)) 92 | 93 | network_config { $name: 94 | ensure => $ensure, 95 | ipaddress => $ipaddress, 96 | netmask => $netmask, 97 | family => $family, 98 | method => $method, 99 | onboot => $onboot, 100 | hotplug => $hotplug, 101 | options => $opts, 102 | } 103 | 104 | network_config { $slaves: 105 | ensure => absent, 106 | reconfigure => true, 107 | before => Network_config[$name], 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/puppet/provider/network_route/redhat.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | require 'puppetx/filemapper' 3 | 4 | Puppet::Type.type(:network_route).provide(:redhat) do 5 | # RHEL network_route routes provider. 6 | # 7 | # This provider uses the filemapper mixin to map the routes file to a 8 | # collection of network_route providers, and back. 9 | # 10 | # @see https://access.redhat.com/knowledge/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/s1-networkscripts-static-routes.html 11 | 12 | include PuppetX::FileMapper 13 | 14 | desc 'RHEL style routes provider' 15 | 16 | confine 'os.family' => :redhat 17 | defaultfor 'os.family' => :redhat 18 | 19 | has_feature :provider_options 20 | 21 | def select_file 22 | "/etc/sysconfig/network-scripts/route-#{interface}" 23 | end 24 | 25 | def self.target_files(script_dir = '/etc/sysconfig/network-scripts') 26 | entries = Dir.entries(script_dir).select { |entry| entry.match %r{route-.*} } 27 | entries.map { |entry| File.join(script_dir, entry) } 28 | end 29 | 30 | def self.parse_file(_filename, contents) 31 | routes = [] 32 | lines = contents.split("\n") 33 | lines.each do |line| 34 | # Strip off any trailing comments 35 | line.sub!(%r{#.*$}, '') 36 | 37 | if line =~ %r{^\s*#|^\s*$} 38 | # Ignore comments and blank lines 39 | next 40 | end 41 | 42 | route = line.split(' ', 6) 43 | raise Puppet::Error, 'Malformed redhat route file, cannot instantiate network_route resources' if route.length < 4 44 | 45 | route = line.split(' ', 5) if route[0] == 'local' 46 | 47 | new_route = {} 48 | 49 | if route[0] == 'local' 50 | new_route[:interface] = route[3] 51 | new_route[:options] = route[4] if route[4] 52 | else 53 | new_route[:gateway] = route[2] 54 | new_route[:interface] = route[4] 55 | new_route[:options] = route[5] if route[5] 56 | end 57 | 58 | if route[0] == 'local' 59 | new_route[:name] = route[1] 60 | new_route[:network] = 'local' 61 | elsif ['default', '0.0.0.0'].include? route[0] 62 | new_route[:name] = 'default' # FIXME: Must match :name in order for changes to be detected 63 | new_route[:network] = 'default' 64 | new_route[:netmask] = '0.0.0.0' 65 | else 66 | ip = IPAddr.new(route[0]) 67 | netmask_addr = ip.prefix <= 32 ? '255.255.255.255' : 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' 68 | netmask = IPAddr.new("#{netmask_addr}/#{ip.prefix}") 69 | new_route[:name] = "#{ip}/#{ip.prefix}" # FIXME: Must match :name in order for changes to be detected 70 | new_route[:network] = ip.to_s 71 | new_route[:netmask] = netmask.to_s 72 | end 73 | routes << new_route 74 | end 75 | 76 | routes 77 | end 78 | 79 | # Generate an array of sections 80 | def self.format_file(_filename, providers) 81 | contents = [] 82 | contents << header 83 | # Build routes 84 | providers.sort_by(&:name).each do |provider| 85 | %w[network interface].each do |prop| 86 | raise Puppet::Error, "#{provider.name} is missing the required parameter '#{prop}'." if provider.send(prop).nil? 87 | end 88 | %w[netmask gateway].each do |prop| 89 | raise Puppet::Error, "#{provider.name} is missing the required parameter '#{prop}'." if provider.send(prop).nil? && provider.send('network').to_s != 'local' 90 | end 91 | contents << if provider.network == 'default' 92 | "#{provider.network} via #{provider.gateway} dev #{provider.interface}" 93 | elsif provider.network == 'local' 94 | "#{provider.network} #{provider.name} dev #{provider.interface}" 95 | else 96 | ip = IPAddr.new("#{provider.network}/#{provider.netmask}") 97 | "#{ip}/#{ip.prefix} via #{provider.gateway} dev #{provider.interface}" 98 | end 99 | contents << (provider.options == :absent ? "\n" : " #{provider.options}\n") 100 | end 101 | contents.join 102 | end 103 | 104 | def self.header 105 | <<~HEADER 106 | # HEADER: This file is being managed by puppet. Changes to 107 | # HEADER: routes that are not being managed by puppet will persist; 108 | # HEADER: however changes to routes that are being managed by puppet will 109 | # HEADER: be overwritten. In addition, file order is NOT guaranteed. 110 | # HEADER: Last generated at: #{Time.now} 111 | HEADER 112 | end 113 | 114 | def self.post_flush_hook(filename) 115 | File.chmod(0o644, filename) 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/defines/bond/debian_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'network::bond::debian', type: :define do 4 | let(:title) { 'bond0' } 5 | 6 | describe 'with default bonding params' do 7 | let(:params) do 8 | { 9 | 'ensure' => 'present', 10 | 'method' => 'static', 11 | 'ipaddress' => '172.18.1.2', 12 | 'netmask' => '255.255.128.0', 13 | 'slaves' => %w[eth0 eth1], 14 | 15 | 'mode' => 'active-backup', 16 | 'miimon' => '100', 17 | 'downdelay' => '200', 18 | 'updelay' => '200', 19 | 'lacp_rate' => 'slow', 20 | 'primary' => 'eth0', 21 | 'primary_reselect' => 'always', 22 | 'xmit_hash_policy' => 'layer2' 23 | } 24 | end 25 | 26 | %w[eth0 eth1].each do |slave| 27 | it "adds a network_config resource for #{slave}" do 28 | is_expected.to contain_network_config(slave).with_ensure('absent') 29 | end 30 | end 31 | 32 | it 'adds a network_config resource for bond0' do 33 | is_expected.to contain_network_config('bond0').with('ensure' => 'present', 34 | 'method' => 'static', 35 | 'ipaddress' => '172.18.1.2', 36 | 'netmask' => '255.255.128.0', 37 | 'options' => { 38 | 'bond-slaves' => 'eth0 eth1', 39 | 'bond-mode' => 'active-backup', 40 | 'bond-miimon' => '100', 41 | 'bond-downdelay' => '200', 42 | 'bond-updelay' => '200', 43 | 'bond-lacp-rate' => 'slow', 44 | 'bond-primary' => 'eth0', 45 | 'bond-primary-reselect' => 'always', 46 | 'bond-xmit-hash-policy' => 'layer2' 47 | }) 48 | end 49 | end 50 | 51 | describe 'with non-default bonding params' do 52 | let(:params) do 53 | { 54 | 'ensure' => 'present', 55 | 'method' => 'static', 56 | 'ipaddress' => '10.20.2.1', 57 | 'netmask' => '255.255.255.192', 58 | 'slaves' => %w[eth0 eth1 eth2], 59 | 'mtu' => 1550, 60 | 'options' => { 'bond-future-option' => 'yes' }, 61 | 'slave_options' => { 'slave-future-option' => 'no' }, 62 | 'hotplug' => 'false', 63 | 64 | 'mode' => 'balance-rr', 65 | 'miimon' => '50', 66 | 'downdelay' => '100', 67 | 'updelay' => '100', 68 | 'lacp_rate' => 'fast', 69 | 'xmit_hash_policy' => 'layer3+4' 70 | } 71 | end 72 | 73 | %w[eth0 eth1 eth2].each do |slave| 74 | it "adds a network_config resource for #{slave}" do 75 | is_expected.to contain_network_config(slave).with_ensure('absent') 76 | end 77 | end 78 | 79 | it 'adds a network_config resource for bond0' do 80 | is_expected.to contain_network_config('bond0').with('ensure' => 'present', 81 | 'method' => 'static', 82 | 'ipaddress' => '10.20.2.1', 83 | 'netmask' => '255.255.255.192', 84 | 'hotplug' => false, 85 | 'options' => { 86 | 'bond-slaves' => 'eth0 eth1 eth2', 87 | 'bond-mode' => 'balance-rr', 88 | 'bond-miimon' => '50', 89 | 'bond-downdelay' => '100', 90 | 'bond-updelay' => '100', 91 | 'bond-lacp-rate' => 'fast', 92 | 'bond-xmit-hash-policy' => 'layer3+4', 93 | 'bond-future-option' => 'yes', 94 | 'post-up' => 'ip link set dev bond0 mtu 1550' 95 | }) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/defines/bond/redhat_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'network::bond::redhat', type: :define do 4 | let(:title) { 'bond0' } 5 | 6 | describe 'with default bonding params' do 7 | let(:params) do 8 | { 9 | 'ensure' => 'present', 10 | 'method' => 'static', 11 | 'ipaddress' => '172.18.1.2', 12 | 'netmask' => '255.255.128.0', 13 | 'slaves' => %w[eth0 eth1], 14 | 15 | 'mode' => 'active-backup', 16 | 'miimon' => '100', 17 | 'downdelay' => '200', 18 | 'updelay' => '200', 19 | 'lacp_rate' => 'slow', 20 | 'primary' => 'eth0', 21 | 'primary_reselect' => 'always', 22 | 'xmit_hash_policy' => 'layer2' 23 | } 24 | end 25 | 26 | %w[eth0 eth1].each do |slave| 27 | it "adds a network_config resource for #{slave}" do 28 | is_expected.to contain_network_config(slave).with('ensure' => 'present', 29 | 'method' => 'static', 30 | 'onboot' => true, 31 | 'hotplug' => false, 32 | 'options' => { 33 | 'MASTER' => 'bond0', 34 | 'SLAVE' => 'yes' 35 | }) 36 | end 37 | end 38 | 39 | it 'adds a network_config resource for bond0' do 40 | is_expected.to contain_network_config('bond0').with('ensure' => 'present', 41 | 'method' => 'static', 42 | 'ipaddress' => '172.18.1.2', 43 | 'netmask' => '255.255.128.0', 44 | 'options' => { 45 | 'BONDING_OPTS' => 'mode=active-backup miimon=100 downdelay=200 updelay=200 lacp_rate=slow primary=eth0 primary_reselect=always xmit_hash_policy=layer2' 46 | }) 47 | end 48 | end 49 | 50 | describe 'with non-default bonding params' do 51 | let(:params) do 52 | { 53 | 'ensure' => 'present', 54 | 'method' => 'static', 55 | 'ipaddress' => '10.20.2.1', 56 | 'netmask' => '255.255.255.192', 57 | 'slaves' => %w[eth0 eth1 eth2], 58 | 'mtu' => '1550', 59 | 'options' => { 'NM_CONTROLLED' => 'yes' }, 60 | 'slave_options' => { 'NM_CONTROLLED' => 'no' }, 61 | 'hotplug' => 'false', 62 | 63 | 'mode' => 'balance-rr', 64 | 'miimon' => '50', 65 | 'downdelay' => '100', 66 | 'updelay' => '100', 67 | 'lacp_rate' => 'fast', 68 | 'xmit_hash_policy' => 'layer3+4' 69 | } 70 | end 71 | 72 | %w[eth0 eth1 eth2].each do |slave| 73 | it "adds a network_config resource for #{slave}" do 74 | is_expected.to contain_network_config(slave).with('ensure' => 'present', 75 | 'method' => 'static', 76 | 'onboot' => true, 77 | 'hotplug' => false, 78 | 'options' => { 79 | 'MASTER' => 'bond0', 80 | 'SLAVE' => 'yes', 81 | 'NM_CONTROLLED' => 'no' 82 | }) 83 | end 84 | end 85 | 86 | it 'adds a network_config resource for bond0' do 87 | is_expected.to contain_network_config('bond0').with('ensure' => 'present', 88 | 'method' => 'static', 89 | 'ipaddress' => '10.20.2.1', 90 | 'netmask' => '255.255.255.192', 91 | 'hotplug' => false, 92 | 'mtu' => '1550', 93 | 'options' => { 94 | 'BONDING_OPTS' => 'mode=balance-rr miimon=50 downdelay=100 updelay=100 lacp_rate=fast xmit_hash_policy=layer3+4', 95 | 'NM_CONTROLLED' => 'yes' 96 | }) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/unit/type/network_route_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Puppet::Type.type(:network_route) do 4 | before do 5 | provider_class = instance_double 'provider class' 6 | allow(provider_class).to receive(:name).and_return('fake') 7 | allow(provider_class).to receive(:suitable?).and_return(true) 8 | allow(provider_class).to receive(:supports_parameter?).and_return(true) 9 | allow(provider_class).to receive(:new) 10 | allow(Puppet::Type.type(:network_route)).to receive(:defaultprovider).and_return(provider_class) 11 | allow(Puppet::Type.type(:network_route)).to receive(:provider).and_return(provider_class) 12 | end 13 | 14 | describe 'when validating the attribute' do 15 | describe :name do # rubocop:disable RSpec/DescribeSymbol 16 | it { expect(Puppet::Type.type(:network_route).attrtype(:name)).to eq(:param) } 17 | end 18 | 19 | %i[ensure network netmask gateway interface options].each do |property| 20 | describe property do 21 | it { expect(Puppet::Type.type(:network_route).attrtype(property)).to eq(:property) } 22 | end 23 | end 24 | 25 | it 'use the name parameter as the namevar' do 26 | expect(Puppet::Type.type(:network_route).key_attributes).to eq([:name]) 27 | end 28 | 29 | describe 'ensure' do 30 | it 'is an ensurable value' do 31 | expect(Puppet::Type.type(:network_route).propertybyname(:ensure).ancestors).to be_include(Puppet::Property::Ensure) 32 | end 33 | end 34 | end 35 | 36 | describe 'when validating the attribute value' do 37 | describe 'network' do 38 | it 'allows local' do 39 | r = Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: 'local', netmask: '255.255.255.0', gateway: '23.23.23.42', interface: 'eth0') 40 | expect(r[:network]).to eq('local') 41 | end 42 | 43 | it 'validates the network as an IP address' do 44 | expect do 45 | Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: 'not an ip address', netmask: '255.255.255.0', gateway: '23.23.23.42', interface: 'eth0') 46 | end.to raise_error(%r{Invalid value for parameter 'network'}) 47 | end 48 | end 49 | 50 | describe 'netmask' do 51 | it 'fails if an invalid netmask is used' do 52 | expect do 53 | Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: '192.168.1.0', netmask: 'This is clearly not a netmask', gateway: '23.23.23.42', interface: 'eth0') 54 | end.to raise_error(%r{Invalid value for parameter 'netmask'}) 55 | end 56 | 57 | it 'converts netmasks of the CIDR form' do 58 | r = Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: '192.168.1.0', netmask: '24', gateway: '23.23.23.42', interface: 'eth0') 59 | expect(r[:netmask]).to eq('255.255.255.0') 60 | end 61 | 62 | it 'converts IPv6 netmasks of the CIDR form' do 63 | r = Puppet::Type.type(:network_route).new(name: 'lxd bridge', network: 'fd58:281b:6eef:eb3d::', netmask: '64', gateway: 'fd58:281b:6eef:eb3d::1', interface: 'lxdbr0') 64 | expect(r[:netmask]).to eq('ffff:ffff:ffff:ffff::') 65 | end 66 | 67 | it 'converts netmasks of the expanded netmask form' do 68 | r = Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: '192.168.1.0', netmask: '255.255.128.0', gateway: '23.23.23.42', interface: 'eth0') 69 | expect(r[:netmask]).to eq('255.255.128.0') 70 | end 71 | 72 | it 'requires netmask when not local' do 73 | expect do 74 | Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: '192.168.1.0', gateway: '10.0.0.1', interface: 'eth0') 75 | end.to raise_error(%r{must have netmask defined}) 76 | end 77 | 78 | it 'does not require netmask when local' do 79 | r = Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: 'local', gateway: '10.0.0.1', interface: 'eth0') 80 | expect(r[:netmask]).to be_nil 81 | end 82 | end 83 | 84 | describe 'gateway' do 85 | it 'validates as an IP address' do 86 | expect do 87 | Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: '192.168.1.0', netmask: '255.255.255.0', gateway: 'not an ip address', interface: 'eth0') 88 | end.to raise_error(%r{Invalid value for parameter 'gateway'}) 89 | end 90 | 91 | it 'requires gateway when not local' do 92 | expect do 93 | Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: '192.168.1.0', netmask: '255.255.255.0', interface: 'eth0') 94 | end.to raise_error(%r{must have gateway defined}) 95 | end 96 | 97 | it 'does not require gateway when local' do 98 | r = Puppet::Type.type(:network_route).new(name: '192.168.1.0/24', network: 'local', netmask: '255.255.255.0', interface: 'eth0') 99 | expect(r[:gateway]).to be_nil 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/puppet/type/network_config.rb: -------------------------------------------------------------------------------- 1 | require 'puppet/property/boolean' 2 | 3 | begin 4 | require 'ipaddr' 5 | rescue LoadError 6 | Puppet.warning("#{__FILE__}:#{__LINE__}: ipaddr gem was not found") 7 | end 8 | 9 | Puppet::Type.newtype(:network_config) do 10 | @doc = 'Manage non-volatile network configuration information' 11 | 12 | feature :provider_options, <<-EOD 13 | The provider can accept a hash of arbitrary options. The semantics of 14 | these options will depend on the provider. 15 | EOD 16 | 17 | feature :hotpluggable, 'The system can hotplug interfaces' 18 | 19 | feature :reconfigurable, <<-EOD 20 | The provider can update live interface configuration after the non-volatile 21 | network configuration is updated. This may entail a momentary network 22 | disruption as it may mean bringing down the interface for a short period. 23 | EOD 24 | 25 | feature :startmode, 'The system can define startmode of an interface' 26 | 27 | ensurable 28 | 29 | newparam(:name) do 30 | isnamevar 31 | desc 'The name of the physical or logical network device' 32 | end 33 | 34 | newproperty(:ipaddress) do 35 | desc 'The IP address of the network interfaces' 36 | if defined? IPAddr 37 | validate do |value| 38 | IPAddr.new value 39 | rescue IPAddr::InvalidAddressError 40 | raise ArgumentError, "#{self.class} requires a valid ipaddress for the ipaddress property" 41 | # provider.validate 42 | end 43 | end 44 | end 45 | 46 | newproperty(:netmask) do 47 | desc 'The subnet mask to apply to the interface' 48 | if defined? IPAddr 49 | validate do |value| 50 | ipa = IPAddr.new '127.0.0.1' 51 | ipa.mask(value) 52 | rescue IPAddr::InvalidAddressError 53 | begin 54 | ipz = IPAddr.new '::1' 55 | ipz.mask(value) 56 | rescue IPAddr::InvalidAddressError 57 | raise ArgumentError, "#{self.class} requires a valid netmask for the netmask property" 58 | end 59 | # provider.validate 60 | end 61 | end 62 | end 63 | 64 | newproperty(:method) do 65 | desc 'The method for determining an IP address for the interface' 66 | newvalues(:static, :manual, :dhcp, :loopback) 67 | 68 | # Redhat systems frequently use 'none' in place of 'static', although 69 | # ultimately any values but dhcp or bootp are ignored and the interface 70 | # is static 71 | aliasvalue(:none, :static) 72 | 73 | defaultto :dhcp 74 | end 75 | 76 | newproperty(:family) do 77 | desc 'The address family to use for the interface' 78 | newvalues(:inet, :inet6) 79 | aliasvalue(:inet4, :inet) 80 | defaultto :inet 81 | end 82 | 83 | newproperty(:onboot, parent: Puppet::Property::Boolean) do 84 | desc 'Whether to bring the interface up on boot' 85 | defaultto :true 86 | end 87 | 88 | newproperty(:hotplug, required_features: :hotpluggable, parent: Puppet::Property::Boolean) do 89 | desc 'Allow/disallow hotplug support for this interface' 90 | defaultto :true 91 | end 92 | 93 | newproperty(:startmode, required_features: :startmode) do 94 | desc 'Allow/disallow startmode support for this interface' 95 | defaultto :auto 96 | end 97 | 98 | newparam(:reconfigure, required_features: :reconfigurable, parent: Puppet::Property::Boolean) do 99 | desc 'Reconfigure the interface after the configuration has been updated' 100 | end 101 | 102 | newproperty(:mtu) do 103 | desc 'The Maximum Transmission Unit size to use for the interface' 104 | validate do |value| 105 | # reject floating point and negative integers 106 | # XXX this lets 1500.0 pass 107 | if value.is_a? Numeric 108 | raise ArgumentError, "#{value} is not a valid mtu (must be a positive integer)" unless value.integer? 109 | else 110 | raise ArgumentError, "#{value} is not a valid mtu (must be a positive integer)" unless value =~ %r{^\d+$} 111 | end 112 | 113 | # Intel 82598 & 82599 chips support MTUs up to 16110; is there any 114 | # hardware in the wild that supports larger frames? 115 | # 116 | # It appears loopback devices routinely have large MTU values; Eg. 65536 117 | # 118 | # Frames small than 64bytes are discarded as runts. Smallest valid MTU 119 | # is 42 with a 802.1q header and 46 without. 120 | min_mtu = 42 121 | max_mtu = 65_536 122 | raise ArgumentError, "#{value} is not in the valid mtu range (#{min_mtu} .. #{max_mtu})" unless (min_mtu..max_mtu).cover?(value.to_i) 123 | end 124 | end 125 | 126 | newproperty(:mode) do 127 | desc 'The exclusive mode the interface should operate in' 128 | # :bond and :bridge may be added in the future 129 | newvalues(:raw, :vlan) 130 | 131 | defaultto :raw 132 | end 133 | 134 | # `:options` provides an arbitrary passthrough for provider properties, so 135 | # that provider specific behavior doesn't clutter up the main type but still 136 | # allows for more powerful actions to be taken. 137 | newproperty(:options, required_features: :provider_options) do 138 | desc 'Provider specific options to be passed to the provider' 139 | 140 | def s?(hash = @is) 141 | hash.keys.sort.map { |key| "#{key} => #{hash[key]}" }.join(', ') 142 | end 143 | 144 | def should_to_s(hash = @should) 145 | hash.keys.sort.map { |key| "#{key} => #{hash[key]}" }.join(', ') 146 | end 147 | 148 | defaultto {} 149 | 150 | validate do |value| 151 | raise ArgumentError, "#{self.class} requires a hash for the 'options' parameter" unless value.is_a? Hash 152 | # provider.validate 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /spec/unit/provider/network_route/sles_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Puppet::Type.type(:network_route).provider(:sles) do 4 | def fixture_data(file) 5 | basedir = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_route', 'sles') 6 | File.read(File.join(basedir, file)) 7 | end 8 | 9 | describe 'when parsing' do 10 | describe 'a simple well formed file' do 11 | let(:data) { described_class.parse_file('', fixture_data('simple_routes')) } 12 | 13 | it 'parses out normal ipv4 network routes' do 14 | expect(data.find { |h| h[:name] == '172.17.67.0/30' }).to eq( 15 | name: '172.17.67.0/30', 16 | network: '172.17.67.0', 17 | netmask: '255.255.255.252', 18 | gateway: '172.18.6.2', 19 | interface: 'vlan200' 20 | ) 21 | end 22 | 23 | it 'parses out ipv6 network routes' do 24 | expect(data.find { |h| h[:name] == '2a01:4f8:211:9d5:53::/96' }).to eq( 25 | name: '2a01:4f8:211:9d5:53::/96', 26 | network: '2a01:4f8:211:9d5:53::', 27 | netmask: 'ffff:ffff:ffff:ffff:ffff:ffff::', 28 | gateway: '2a01:4f8:211:9d5::2', 29 | interface: 'vlan200' 30 | ) 31 | end 32 | 33 | it 'parses out default routes' do 34 | expect(data.find { |h| h[:name] == 'default' }).to eq( 35 | name: 'default', 36 | network: 'default', 37 | netmask: '0.0.0.0', 38 | gateway: '10.0.0.1', 39 | interface: 'eth1' 40 | ) 41 | end 42 | end 43 | 44 | describe 'an advanced, well formed file' do 45 | let :data do 46 | described_class.parse_file('', fixture_data('advanced_routes')) 47 | end 48 | 49 | it 'parses out normal ipv4 network routes' do 50 | expect(data.find { |h| h[:name] == '2a01:4f8:211:9d5:53::/96' }).to eq( 51 | name: '2a01:4f8:211:9d5:53::/96', 52 | network: '2a01:4f8:211:9d5:53::', 53 | netmask: 'ffff:ffff:ffff:ffff:ffff:ffff::', 54 | gateway: '2a01:4f8:211:9d5::2', 55 | interface: 'vlan200', 56 | options: 'table 200' 57 | ) 58 | end 59 | 60 | it 'parses out normal ipv6 network routes' do 61 | expect(data.find { |h| h[:name] == '172.17.67.0/30' }).to eq( 62 | name: '172.17.67.0/30', 63 | network: '172.17.67.0', 64 | netmask: '255.255.255.252', 65 | gateway: '172.18.6.2', 66 | interface: 'vlan200', 67 | options: 'table 200' 68 | ) 69 | end 70 | end 71 | 72 | describe 'an invalid file' do 73 | it 'fails' do 74 | expect do 75 | described_class.parse_file('', "192.168.1.1/30 via\n") 76 | end.to raise_error(%r{Malformed SLES route file}) 77 | end 78 | end 79 | end 80 | 81 | describe 'when formatting' do 82 | let :route1_provider do 83 | instance_double( 84 | 'route1_provider', 85 | name: '172.17.67.0/30', 86 | network: '172.17.67.0', 87 | netmask: '30', 88 | gateway: '172.18.6.2', 89 | interface: 'vlan200', 90 | options: 'table 200' 91 | ) 92 | end 93 | 94 | let :route2_provider do 95 | instance_double( 96 | 'lo_provider', 97 | name: '172.28.45.0/30', 98 | network: '172.28.45.0', 99 | netmask: '30', 100 | gateway: '172.18.6.2', 101 | interface: 'eth0', 102 | options: 'table 200' 103 | ) 104 | end 105 | 106 | let :defaultroute_provider do 107 | instance_double( 108 | 'defaultroute_provider', 109 | name: 'default', 110 | network: 'default', 111 | netmask: '', 112 | gateway: '10.0.0.1', 113 | interface: 'eth1', 114 | options: 'table 200' 115 | ) 116 | end 117 | 118 | let :nooptions_provider do 119 | instance_double( 120 | 'nooptions_provider', 121 | name: 'default', 122 | network: 'default', 123 | netmask: '', 124 | gateway: '10.0.0.1', 125 | interface: 'eth2', 126 | options: :absent 127 | ) 128 | end 129 | 130 | let :content do 131 | described_class.format_file('', [route1_provider, route2_provider, defaultroute_provider, nooptions_provider]) 132 | end 133 | 134 | describe 'writing the route line' do 135 | describe 'For standard (non-default) routes' do 136 | it 'writes a single line for the route' do 137 | expect(content.scan(%r{^172.17.67.0/30 .*$}).length).to eq(1) 138 | end 139 | 140 | it 'writes 6 fields' do 141 | expect(content.scan(%r{^172.17.67.0/30 .*$}).first.split.length).to eq(6) 142 | end 143 | 144 | it 'has the correct fields appended' do 145 | expect(content.scan(%r{^172.17.67.0/30 .*$}).first).to include('172.17.67.0/30 172.18.6.2 - vlan200 table 200') 146 | end 147 | 148 | it 'fails if the netmask property is not defined' do 149 | allow(route2_provider).to receive(:netmask).and_return(nil) 150 | expect { content }.to raise_exception(%r{is missing the required parameter 'netmask'}) 151 | end 152 | 153 | it 'fails if the gateway property is not defined' do 154 | allow(route2_provider).to receive(:gateway).and_return(nil) 155 | expect { content }.to raise_exception(%r{is missing the required parameter 'gateway'}) 156 | end 157 | end 158 | end 159 | 160 | describe 'for default routes' do 161 | it 'has the correct fields appended' do 162 | expect(content.scan(%r{^default .*$}).first).to include('default 10.0.0.1 - eth1') 163 | end 164 | 165 | it 'does not contain the word absent when no options are defined' do 166 | expect(content).not_to match(%r{absent}) 167 | end 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Network module for Puppet 2 | 3 | [![Build Status](https://github.com/voxpupuli/puppet-network/workflows/CI/badge.svg)](https://github.com/voxpupuli/puppet-network/actions?query=workflow%3ACI) 4 | [![Release](https://github.com/voxpupuli/puppet-network/actions/workflows/release.yml/badge.svg)](https://github.com/voxpupuli/puppet-network/actions/workflows/release.yml) 5 | [![Puppet Forge](https://img.shields.io/puppetforge/v/puppet/network.svg)](https://forge.puppetlabs.com/puppet/network) 6 | [![Puppet Forge - downloads](https://img.shields.io/puppetforge/dt/puppet/network.svg)](https://forge.puppetlabs.com/puppet/network) 7 | [![Puppet Forge - endorsement](https://img.shields.io/puppetforge/e/puppet/network.svg)](https://forge.puppetlabs.com/puppet/network) 8 | [![Puppet Forge - scores](https://img.shields.io/puppetforge/f/puppet/network.svg)](https://forge.puppetlabs.com/puppet/network) 9 | [![puppetmodule.info docs](http://www.puppetmodule.info/images/badge.png)](http://www.puppetmodule.info/m/puppet-network) 10 | [![Apache-2 License](https://img.shields.io/github/license/voxpupuli/puppet-network.svg)](LICENSE) 11 | 12 | ## Overview 13 | 14 | Manage non-volatile network and route configuration. 15 | 16 | ## Usage 17 | 18 | > [!NOTE] 19 | > This module defines custom types (`network_config` and `network_route`) that require proper plugin synchronization. If you encounter errors like "Could not autoload puppet/type/network_config", you may need to run `puppet generate types` on your Puppet server (for Puppet 6+) or `puppet plugin download` (for older versions). See [issue #77](https://github.com/voxpupuli/puppet-network/issues/77) for more details. 20 | 21 | Interface configuration 22 | 23 | ```puppet 24 | network_config { 'eth0': 25 | ensure => 'present', 26 | family => 'inet', 27 | method => 'dhcp', 28 | onboot => 'true', 29 | hotplug => 'true', 30 | options => {'pre-up' => 'sleep 2'}, 31 | } 32 | 33 | network_config { 'lo': 34 | ensure => 'present', 35 | family => 'inet', 36 | method => 'loopback', 37 | onboot => 'true', 38 | } 39 | 40 | network_config { 'eth1': 41 | ensure => 'present', 42 | family => 'inet', 43 | ipaddress => '169.254.0.1', 44 | method => 'static', 45 | netmask => '255.255.0.0', 46 | onboot => 'true', 47 | } 48 | ``` 49 | 50 | Route configuration 51 | 52 | Route resources should be named in CIDR notation. If not, they will not be 53 | properly mapped to existing routes and puppet will apply them on every run. 54 | Default routes should be named 'default'. 55 | 56 | For Debian: 57 | 58 | ```puppet 59 | # default route 60 | network_route { 'default': 61 | ensure => 'present', 62 | network => 'default', 63 | netmask => '0.0.0.0', 64 | gateway => '172.18.6.2', 65 | interface => 'enp3s0f0', 66 | } 67 | 68 | # specific route 69 | network_route { '172.17.67.0/24': 70 | ensure => 'present', 71 | gateway => '172.18.6.2', 72 | interface => 'vlan200', 73 | netmask => '255.255.255.0', 74 | options => 'table 200', 75 | } 76 | ``` 77 | 78 | For RedHat Enterprise: 79 | 80 | ```puppet 81 | network_route { '172.17.67.0/24': 82 | ensure => 'present', 83 | gateway => '10.0.2.2', 84 | interface => 'eth0', 85 | netmask => '255.255.255.0', 86 | network => '172.17.67.0', 87 | options => 'table 200', 88 | } 89 | network_route { 'default': 90 | ensure => 'present', 91 | gateway => '10.0.2.2', 92 | interface => 'eth0', 93 | netmask => '0.0.0.0', 94 | network => 'default' 95 | } 96 | network_route { '10.0.0.2': 97 | ensure => 'present', 98 | network => 'local', 99 | interface => 'eth0', 100 | options => 'proto 66 scope host table local', 101 | } 102 | ``` 103 | 104 | For SLES: 105 | 106 | ```puppet 107 | network_route { 'default': 108 | ensure => 'present', 109 | gateway => '10.0.2.2', 110 | interface => 'eth0', 111 | netmask => '0.0.0.0', 112 | network => 'default' 113 | } 114 | ``` 115 | 116 | Create resources on the fly with the `puppet resource` command: 117 | 118 | root@debian-6:~# puppet resource network_config eth1 ensure=present family=inet method=static ipaddress=169.254.0.1 netmask=255.255.0.0 119 | notice: /Network_config[eth1]/ensure: created 120 | network_config { 'eth1': 121 | ensure => 'present', 122 | family => 'inet', 123 | ipaddress => '169.254.0.1', 124 | method => 'static', 125 | netmask => '255.255.0.0', 126 | onboot => 'true', 127 | } 128 | 129 | # puppet resource network_route 23.23.42.0 ensure=present netmask=255.255.255.0 interface=eth0 gateway=192.168.1.1 130 | notice: /Network_route[23.23.42.0]/ensure: created 131 | network_route { '23.23.42.0': 132 | ensure => 'present', 133 | gateway => '192.168.1.1', 134 | interface => 'eth0', 135 | netmask => '255.255.255.0', 136 | options => 'table 200', 137 | } 138 | 139 | ## Dependencies 140 | 141 | This module requires the FileMapper mixin, available at . 142 | 143 | The debian routes provider requires the package [ifupdown-extra](http://packages.debian.org/search?suite=all§ion=all&arch=any&searchon=names&keywords=ifupdown-extra). 144 | `ifupdown-extra` can be installed automatically using the `network` class. 145 | To use it, include it like so in your manifests: 146 | 147 | ```puppet 148 | include 'network' 149 | ``` 150 | 151 | This class also provides fine-grained control over which packages to install and 152 | how to install them. The documentation for the parameters exposed can be found 153 | [here](https://github.com/voxpupuli/puppet-network/blob/master/manifests/init.pp). 154 | 155 | Bonding on Debian requires the package [ifenslave](https://packages.debian.org/search?suite=all§ion=all&arch=any&searchon=names&keywords=ifenslave), 156 | which is installed automatically when a bond is defined. This package was 157 | renamed in Debian 9, and therefore bonding does not work on Debian 158 | versions prior to 9. 159 | 160 | Note: you may also need to update your master's plugins (run on your puppet master): 161 | 162 | puppet agent -t --noop 163 | 164 | Or on puppet 3.8.7/4.x: 165 | 166 | puppet plugin download 167 | 168 | - - - 169 | 170 | ## Contact 171 | 172 | * Source code: 173 | * Issue tracker: 174 | -------------------------------------------------------------------------------- /spec/unit/provider/network_route/routes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Puppet::Type.type(:network_route).provider(:routes) do 4 | def fixture_data(file) 5 | basedir = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_route', 'routes_spec') 6 | File.read(File.join(basedir, file)) 7 | end 8 | 9 | describe 'when parsing' do 10 | it 'parses out simple ipv4 iface lines' do 11 | fixture = fixture_data('simple_routes') 12 | data = described_class.parse_file('', fixture) 13 | 14 | expect(data.find { |h| h[:name] == '172.17.67.0/24' }).to eq( 15 | name: '172.17.67.0/24', 16 | network: '172.17.67.0', 17 | netmask: '255.255.255.0', 18 | gateway: '172.18.6.2', 19 | interface: 'vlan200' 20 | ) 21 | end 22 | 23 | it 'names default routes "default" and have a 0.0.0.0 netmask' do 24 | fixture = fixture_data('simple_routes') 25 | data = described_class.parse_file('', fixture) 26 | 27 | expect(data.find { |h| h[:name] == 'default' }).to eq( 28 | name: 'default', 29 | network: 'default', 30 | netmask: '0.0.0.0', 31 | gateway: '172.18.6.2', 32 | interface: 'vlan200' 33 | ) 34 | end 35 | 36 | it 'parses out simple ipv6 iface lines' do 37 | fixture = fixture_data('simple_routes') 38 | data = described_class.parse_file('', fixture) 39 | 40 | expect(data.find { |h| h[:name] == '2a01:4f8:211:9d5:53::/96' }).to eq( 41 | name: '2a01:4f8:211:9d5:53::/96', 42 | network: '2a01:4f8:211:9d5:53::', 43 | netmask: 'ffff:ffff:ffff:ffff:ffff:ffff::', 44 | gateway: '2a01:4f8:211:9d5::2', 45 | interface: 'vlan200' 46 | ) 47 | end 48 | 49 | it 'parses out advanced routes' do 50 | fixture = fixture_data('advanced_routes') 51 | data = described_class.parse_file('', fixture) 52 | 53 | expect(data.find { |h| h[:name] == '172.17.67.0/24' }).to eq( 54 | name: '172.17.67.0/24', 55 | network: '172.17.67.0', 56 | netmask: '255.255.255.0', 57 | gateway: '172.18.6.2', 58 | interface: 'vlan200', 59 | options: 'table 200' 60 | ) 61 | end 62 | 63 | it 'parses out advanced ipv6 iface lines' do 64 | fixture = fixture_data('advanced_routes') 65 | data = described_class.parse_file('', fixture) 66 | 67 | expect(data.find { |h| h[:name] == '2a01:4f8:211:9d5:53::/96' }).to eq( 68 | name: '2a01:4f8:211:9d5:53::/96', 69 | network: '2a01:4f8:211:9d5:53::', 70 | netmask: 'ffff:ffff:ffff:ffff:ffff:ffff::', 71 | gateway: '2a01:4f8:211:9d5::2', 72 | interface: 'vlan200', 73 | options: 'table 200' 74 | ) 75 | end 76 | 77 | describe 'when reading an invalid routes file' do 78 | it 'with missing options should fail' do 79 | expect do 80 | described_class.parse_file('', "192.168.1.1 255.255.255.0 172.16.0.1\n") 81 | end.to raise_error(%r{Malformed debian routes file}) 82 | end 83 | end 84 | end 85 | 86 | describe 'when formatting' do 87 | let :route1_provider do 88 | instance_double( 89 | 'route1_provider', 90 | name: '172.17.67.0', 91 | network: '172.17.67.0', 92 | netmask: '255.255.255.0', 93 | gateway: '172.18.6.2', 94 | interface: 'vlan200', 95 | options: 'table 200' 96 | ) 97 | end 98 | 99 | let :route2_provider do 100 | instance_double( 101 | 'lo_provider', 102 | name: '172.28.45.0', 103 | network: '172.28.45.0', 104 | netmask: '255.255.255.0', 105 | gateway: '172.18.6.2', 106 | interface: 'eth0', 107 | options: 'table 200' 108 | ) 109 | end 110 | 111 | let :content do 112 | described_class.format_file('', [route1_provider, route2_provider]) 113 | end 114 | 115 | describe 'writing the route line' do 116 | it 'writes a single line for the route' do 117 | expect(content.scan(%r{^172.17.67.0 .*$}).length).to eq(1) 118 | end 119 | 120 | it 'writes all 6 fields' do 121 | expect(content.scan(%r{^172.17.67.0 .*$}).first.split(' ').length).to eq(6) 122 | end 123 | 124 | it 'has the correct fields appended' do 125 | expect(content.scan(%r{^172.17.67.0 .*$}).first).to include('172.17.67.0 255.255.255.0 172.18.6.2 vlan200 table 200') 126 | end 127 | 128 | it 'fails if the netmask property is not defined' do 129 | allow(route2_provider).to receive(:netmask).and_return(nil) 130 | expect { content }.to raise_exception(%r{is missing the required parameter 'netmask'}) 131 | end 132 | 133 | it 'fails if the gateway property is not defined' do 134 | allow(route2_provider).to receive(:gateway).and_return(nil) 135 | expect { content }.to raise_exception(%r{is missing the required parameter 'gateway'}) 136 | end 137 | end 138 | end 139 | 140 | describe 'when formatting simple files' do 141 | let :route1_provider do 142 | instance_double( 143 | 'route1_provider', 144 | name: '172.17.67.0', 145 | network: '172.17.67.0', 146 | netmask: '255.255.255.0', 147 | gateway: '172.18.6.2', 148 | interface: 'vlan200', 149 | options: :absent 150 | ) 151 | end 152 | 153 | let :route2_provider do 154 | instance_double( 155 | 'lo_provider', 156 | name: '172.28.45.0', 157 | network: '172.28.45.0', 158 | netmask: '255.255.255.0', 159 | gateway: '172.18.6.2', 160 | interface: 'eth0', 161 | options: :absent 162 | ) 163 | end 164 | 165 | let :content do 166 | described_class.format_file('', [route1_provider, route2_provider]) 167 | end 168 | 169 | describe 'writing the route line' do 170 | it 'writes a single line for the route' do 171 | expect(content.scan(%r{^172.17.67.0 .*$}).length).to eq(1) 172 | end 173 | 174 | it 'writes only fields' do 175 | expect(content.scan(%r{^172.17.67.0 .*$}).first.split(' ').length).to eq(4) 176 | end 177 | 178 | it 'has the correct fields appended' do 179 | expect(content.scan(%r{^172.17.67.0 .*$}).first).to include('172.17.67.0 255.255.255.0 172.18.6.2 vlan200') 180 | end 181 | 182 | it 'fails if the netmask property is not defined' do 183 | allow(route2_provider).to receive(:netmask).and_return(nil) 184 | expect { content }.to raise_exception(%r{is missing the required parameter 'netmask'}) 185 | end 186 | 187 | it 'fails if the gateway property is not defined' do 188 | allow(route2_provider).to receive(:gateway).and_return(nil) 189 | expect { content }.to raise_exception(%r{is missing the required parameter 'gateway'}) 190 | end 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /spec/unit/provider/network_route/redhat_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Puppet::Type.type(:network_route).provider(:redhat) do 4 | def fixture_data(file) 5 | basedir = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_route', 'redhat') 6 | File.read(File.join(basedir, file)) 7 | end 8 | 9 | describe 'when parsing' do 10 | describe 'a simple well formed file' do 11 | let(:data) { described_class.parse_file('', fixture_data('simple_routes')) } 12 | 13 | it 'parses out normal ipv4 network routes' do 14 | expect(data.find { |h| h[:name] == '172.17.67.0/30' }).to eq( 15 | name: '172.17.67.0/30', 16 | network: '172.17.67.0', 17 | netmask: '255.255.255.252', 18 | gateway: '172.18.6.2', 19 | interface: 'vlan200' 20 | ) 21 | end 22 | 23 | it 'parses out ipv6 network routes' do 24 | expect(data.find { |h| h[:name] == '2a01:4f8:211:9d5:53::/96' }).to eq( 25 | name: '2a01:4f8:211:9d5:53::/96', 26 | network: '2a01:4f8:211:9d5:53::', 27 | netmask: 'ffff:ffff:ffff:ffff:ffff:ffff::', 28 | gateway: '2a01:4f8:211:9d5::2', 29 | interface: 'vlan200' 30 | ) 31 | end 32 | 33 | it 'parses out default routes' do 34 | expect(data.find { |h| h[:name] == 'default' }).to eq( 35 | name: 'default', 36 | network: 'default', 37 | netmask: '0.0.0.0', 38 | gateway: '10.0.0.1', 39 | interface: 'eth1' 40 | ) 41 | end 42 | end 43 | 44 | describe 'an advanced, well formed file' do 45 | let :data do 46 | described_class.parse_file('', fixture_data('advanced_routes')) 47 | end 48 | 49 | it 'parses out normal ipv4 network routes' do 50 | expect(data.find { |h| h[:name] == '2a01:4f8:211:9d5:53::/96' }).to eq( 51 | name: '2a01:4f8:211:9d5:53::/96', 52 | network: '2a01:4f8:211:9d5:53::', 53 | netmask: 'ffff:ffff:ffff:ffff:ffff:ffff::', 54 | gateway: '2a01:4f8:211:9d5::2', 55 | interface: 'vlan200', 56 | options: 'table 200' 57 | ) 58 | end 59 | 60 | it 'parses out normal ipv6 network routes' do 61 | expect(data.find { |h| h[:name] == '172.17.67.0/30' }).to eq( 62 | name: '172.17.67.0/30', 63 | network: '172.17.67.0', 64 | netmask: '255.255.255.252', 65 | gateway: '172.18.6.2', 66 | interface: 'vlan200', 67 | options: 'table 200' 68 | ) 69 | end 70 | end 71 | 72 | describe 'an advanced file using local route' do 73 | let :data do 74 | described_class.parse_file('', fixture_data('local_routes')) 75 | end 76 | 77 | it 'parses out normal ipv4 network routes' do 78 | expect(data.find { |h| h[:name] == '10.0.0.2' }).to eq( 79 | name: '10.0.0.2', 80 | network: 'local', 81 | interface: 'eth0', 82 | options: 'proto 66 scope host table local' 83 | ) 84 | end 85 | end 86 | 87 | describe 'an invalid file' do 88 | it 'fails' do 89 | expect do 90 | described_class.parse_file('', "192.168.1.1/30 via\n") 91 | end.to raise_error(%r{Malformed redhat route file}) 92 | end 93 | end 94 | end 95 | 96 | describe 'when formatting' do 97 | let :route1_provider do 98 | instance_double( 99 | 'route1_provider', 100 | name: '172.17.67.0/30', 101 | network: '172.17.67.0', 102 | netmask: '30', 103 | gateway: '172.18.6.2', 104 | interface: 'vlan200', 105 | options: 'table 200' 106 | ) 107 | end 108 | 109 | let :route2_provider do 110 | instance_double( 111 | 'lo_provider', 112 | name: '172.28.45.0/30', 113 | network: '172.28.45.0', 114 | netmask: '30', 115 | gateway: '172.18.6.2', 116 | interface: 'eth0', 117 | options: 'table 200' 118 | ) 119 | end 120 | 121 | let :defaultroute_provider do 122 | instance_double( 123 | 'defaultroute_provider', 124 | name: 'default', 125 | network: 'default', 126 | netmask: '', 127 | gateway: '10.0.0.1', 128 | interface: 'eth1', 129 | options: 'table 200' 130 | ) 131 | end 132 | 133 | let :nooptions_provider do 134 | instance_double( 135 | 'nooptions_provider', 136 | name: 'default', 137 | network: 'default', 138 | netmask: '', 139 | gateway: '10.0.0.1', 140 | interface: 'eth2', 141 | options: :absent 142 | ) 143 | end 144 | 145 | let :local_provider do 146 | instance_double( 147 | 'local_provider', 148 | name: '10.0.0.2', 149 | network: 'local', 150 | netmask: nil, 151 | gateway: nil, 152 | interface: 'eth0', 153 | options: 'proto 66 scope host table local' 154 | ) 155 | end 156 | 157 | let :content do 158 | described_class.format_file('', [route1_provider, route2_provider, defaultroute_provider, nooptions_provider, local_provider]) 159 | end 160 | 161 | describe 'writing the route line' do 162 | describe 'For standard (non-default) routes' do 163 | it 'writes a single line for the route' do 164 | expect(content.scan(%r{^172.17.67.0/30 .*$}).length).to eq(1) 165 | end 166 | 167 | it 'writes 7 fields' do 168 | expect(content.scan(%r{^172.17.67.0/30 .*$}).first.split(' ').length).to eq(7) 169 | end 170 | 171 | it 'has the correct fields appended' do 172 | expect(content.scan(%r{^172.17.67.0/30 .*$}).first).to include('172.17.67.0/30 via 172.18.6.2 dev vlan200 table 200') 173 | end 174 | 175 | it 'fails if the netmask property is not defined' do 176 | allow(route2_provider).to receive(:netmask).and_return(nil) 177 | expect { content }.to raise_exception(%r{is missing the required parameter 'netmask'}) 178 | end 179 | 180 | it 'fails if the gateway property is not defined' do 181 | allow(route2_provider).to receive(:gateway).and_return(nil) 182 | expect { content }.to raise_exception(%r{is missing the required parameter 'gateway'}) 183 | end 184 | end 185 | end 186 | 187 | describe 'for default routes' do 188 | it 'has the correct fields appended' do 189 | expect(content.scan(%r{^default .*$}).first).to include('default via 10.0.0.1 dev eth1') 190 | end 191 | 192 | it 'does not contain the word absent when no options are defined' do 193 | expect(content).not_to match(%r{absent}) 194 | end 195 | end 196 | 197 | describe 'for local routes' do 198 | it 'has local route defined' do 199 | expect(content.scan(%r{^local .*$}).first).to include('local 10.0.0.2 dev eth0 proto 66 scope host table local') 200 | end 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /manifests/bond.pp: -------------------------------------------------------------------------------- 1 | # @summary Instantiate cross-platform bonded interfaces 2 | # 3 | # @param slaves 4 | # A list of slave interfaces to combine for the bonded interface 5 | # @param ensure 6 | # The ensure value for the bonding interface 7 | # @param ipaddress 8 | # The IPv4 address of the interface 9 | # @param netmask 10 | # The IPv4 network mask of the interface 11 | # @param method 12 | # The network configuration method 13 | # @param family 14 | # The IP family (inet or inet6) 15 | # @param onboot 16 | # Whether to bring the interface up on boot 17 | # @param hotplug 18 | # Whether to allow hotplug for the interface 19 | # @param lacp_rate 20 | # Option specifying the rate in which we'll ask our link partner to transmit 21 | # LACPDU packets in 802.3ad mode. Only applicable when mode is '802.3ad'. 22 | # Options: slow/0 (every 30 seconds), fast/1 (every 1 second) 23 | # @param mtu 24 | # The Maximum Transmission Unit size to use for bond interface and all slaves 25 | # @param options 26 | # Hash with custom interfaces options 27 | # @param slave_options 28 | # Hash with custom slave interfaces options 29 | # @param mode 30 | # The interface bond mode. The value can be either the human readable term, 31 | # such as 'active-backup' or the numeric representation, such as '1'. 32 | # Options: balance-rr/0, active-backup/1, balance-xor/2, broadcast/3, 33 | # 802.3ad/4, balance-tlb/5, balance-alb/6 34 | # @param miimon 35 | # The MII link monitoring frequency in milliseconds 36 | # @param downdelay 37 | # The time in milliseconds, to wait before disabling a slave after a link 38 | # failure has been detected 39 | # @param updelay 40 | # The time in milliseconds to wait before enabling a slave after a link 41 | # recovery has been detected 42 | # @param primary 43 | # The primary interface to use when the mode is 'active-backup'. All other 44 | # interfaces will be offline by default. Only applicable when mode is '802.3ad' 45 | # @param primary_reselect 46 | # Specifies the reselection policy for the primary slave. This affects how 47 | # the primary slave is chosen to become the active slave when failure of the 48 | # active slave or recovery of the primary slave occurs. This option is 49 | # designed to prevent flip-flopping between the primary slave and other slaves. 50 | # Options: always/0, better/1, failure/2 51 | # @param xmit_hash_policy 52 | # Selects the transmit hash policy to use for slave selection in balance-xor 53 | # and 802.3ad modes. Options: layer2, layer2+3, layer3+4 54 | # 55 | # @example 56 | # network::bond { 'bond0': 57 | # ipaddress => '172.16.1.2', 58 | # netmask => '255.255.128.0', 59 | # ensure => present, 60 | # slaves => ['eth0', 'eth1'], 61 | # } 62 | # 63 | # @see https://www.kernel.org/doc/Documentation/networking/bonding.txt Linux Ethernet Bonding Driver HOWTO, Section 2 "Bonding Driver Options" 64 | # 65 | define network::bond ( 66 | Array[String[1]] $slaves, 67 | Stdlib::Ensure::Package $ensure = present, 68 | Optional[Stdlib::IP::Address::Nosubnet] $ipaddress = undef, 69 | Optional[String[1]] $netmask = undef, 70 | Optional[String[1]] $method = undef, 71 | Optional[Enum['inet', 'inet6']] $family = undef, 72 | Optional[Boolean] $onboot = undef, 73 | Optional[Variant[Boolean, Enum['true', 'false']]] $hotplug = undef, 74 | Optional[Enum['slow', 'fast', '0', '1']] $lacp_rate = undef, 75 | Optional[ 76 | Variant[Integer[42, 65536], Pattern[/^\d+$/]] 77 | ] $mtu = undef, 78 | Optional[Hash[String, Any]] $options = undef, 79 | Optional[Hash[String, Any]] $slave_options = undef, 80 | Enum[ 81 | 'balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 82 | 'balance-tlb', 'balance-alb', '0', '1', '2', '3','4', '5', '6' 83 | ] $mode = 'active-backup', 84 | String[1] $miimon = '100', 85 | String[1] $downdelay = '200', 86 | String[1] $updelay = '200', 87 | String[1] $primary = $slaves[0], 88 | Enum['always', 'better', 'failure', '0', '1', '2'] $primary_reselect = 'always', 89 | Enum['layer2', 'layer2+3', 'layer3+4'] $xmit_hash_policy = 'layer2', 90 | ) { 91 | require network::bond::setup 92 | 93 | kmod::alias { $name: 94 | ensure => $ensure, 95 | source => 'bonding', 96 | } 97 | 98 | case $facts['os']['family'] { 99 | 'Debian': { 100 | network::bond::debian { $name: 101 | ensure => $ensure, 102 | slaves => $slaves, 103 | ipaddress => $ipaddress, 104 | netmask => $netmask, 105 | method => $method, 106 | family => $family, 107 | onboot => $onboot, 108 | hotplug => $hotplug, 109 | mtu => $mtu, 110 | options => $options, 111 | slave_options => $slave_options, 112 | 113 | mode => $mode, 114 | miimon => $miimon, 115 | downdelay => $downdelay, 116 | updelay => $updelay, 117 | lacp_rate => $lacp_rate, 118 | primary => $primary, 119 | primary_reselect => $primary_reselect, 120 | xmit_hash_policy => $xmit_hash_policy, 121 | 122 | require => Kmod::Alias[$name], 123 | } 124 | } 125 | 'RedHat': { 126 | network::bond::redhat { $name: 127 | ensure => $ensure, 128 | slaves => $slaves, 129 | ipaddress => $ipaddress, 130 | netmask => $netmask, 131 | family => $family, 132 | method => $method, 133 | onboot => $onboot, 134 | hotplug => $hotplug, 135 | mtu => $mtu, 136 | options => $options, 137 | slave_options => $slave_options, 138 | 139 | mode => $mode, 140 | miimon => $miimon, 141 | downdelay => $downdelay, 142 | updelay => $updelay, 143 | lacp_rate => $lacp_rate, 144 | primary => $primary, 145 | primary_reselect => $primary_reselect, 146 | xmit_hash_policy => $xmit_hash_policy, 147 | 148 | require => Kmod::Alias[$name], 149 | } 150 | } 151 | default: { 152 | fail("network::bond does not support osfamily '${facts['os']['family']}'") 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/puppet/provider/network_config/sles.rb: -------------------------------------------------------------------------------- 1 | require 'puppetx/filemapper' 2 | 3 | Puppet::Type.type(:network_config).provide(:sles) do 4 | # SLES network_config network scripts provider. 5 | # 6 | # This provider manages the contents of /etc/networks/ifcfg-* to 7 | # manage non-volatile network configuration. 8 | # 9 | # @see https://documentation.suse.com/sles/15-SP1/html/SLES-all/cha-network.html#sec-network-manconf-files-ifcfg 10 | 11 | include PuppetX::FileMapper 12 | 13 | desc 'SLES network-scripts provider' 14 | 15 | confine 'os.family' => :suse 16 | defaultfor 'os.family' => :suse 17 | 18 | has_feature :startmode 19 | has_feature :provider_options 20 | 21 | # @return [String] The path to network-script directory on sles systems 22 | SLES_SCRIPT_DIRECTORY = '/etc/sysconfig/network'.freeze # rubocop:disable Lint/ConstantDefinitionInBlock 23 | 24 | # The valid vlan ID range is 0-4095; 4096 is out of range 25 | SLES_VLAN_RANGE_REGEX = %r{[1-3]?\d{1,3}|40[0-8]\d|409[0-5]}.freeze # rubocop:disable Lint/ConstantDefinitionInBlock 26 | 27 | # aliases are almost free game, sles rejects some, and max total length is 15 characters 28 | # 15 minus at least 2 for the interface name, and a colon leaves 12 characters for the alias 29 | SLES_ALIAS_REGEX = %r{.{1,12}(? 'eth1') 50 | # prov.select_file # => '/etc/sysconfig/network/ifcfg-eth1' 51 | # 52 | def select_file 53 | "#{SLES_SCRIPT_DIRECTORY}/ifcfg-#{name}" 54 | end 55 | 56 | # Scan all files in the networking directory for interfaces 57 | # 58 | # @param script_dir [String] The path to the networking scripts, defaults to 59 | # {#SLES_SCRIPT_DIRECTORY} 60 | # 61 | # @return [Array] All network-script config files on this machine. 62 | # 63 | # @example 64 | # SLESProvider.target_files 65 | # # => ['/etc/sysconfig/network/ifcfg-eth0', '/etc/sysconfig/network/ifcfg-eth1'] 66 | def self.target_files(script_dir = SLES_SCRIPT_DIRECTORY) 67 | entries = Dir.entries(script_dir).select { |entry| entry.match SCRIPT_REGEX } 68 | entries.map { |entry| File.join(SLES_SCRIPT_DIRECTORY, entry) } 69 | end 70 | 71 | # Convert a SLES network script into a hash 72 | # 73 | # This is a hook method that will be called by PuppetX::Filemapper 74 | # 75 | # @param [String] filename The path of the interfaces file being parsed 76 | # @param [String] contents The contents of the given file 77 | # 78 | # @return [Array>] A single element array containing 79 | # the key/value pairs of properties parsed from the file. 80 | # 81 | # @example 82 | # SLESProvider.parse_file('/etc/sysconfig/network/ifcfg-eth0', #) 83 | # # => [ 84 | # # { 85 | # # :name => 'eth0', 86 | # # :ipaddress => '169.254.0.1', 87 | # # :netmask => '255.255.0.0', 88 | # # }, 89 | # # ] 90 | def self.parse_file(filename, contents) 91 | # Split up the file into lines 92 | lines = contents.split("\n") 93 | # Strip out all comments 94 | lines.map! { |line| line.sub(%r{#.*$}, '') } 95 | # Remove all blank lines 96 | lines.reject! { |line| line =~ %r{^\s*$} } 97 | 98 | pair_regex = %r{^\s*(.+?)\s*=\s*(.*)\s*$} 99 | 100 | # INFO: It is valid ifcfg format to include certain functions and strings these should not be reported as errors, but silently removed before parsing 101 | # https://bugs.centos.org/bug_view_page.php?bug_id=475 102 | # Remove lines with no = sign 103 | lines.select! { |line| line =~ %r{^.*=.*$} } 104 | 105 | # Convert the data into key/value pairs 106 | pairs = lines.each_with_object({}) do |line, hash| 107 | raise Puppet::Error, %(#{filename} is malformed; "#{line}" did not match "#{pair_regex}") unless line.match(pair_regex) do |m| 108 | key = m[1].strip 109 | val = m[2].strip 110 | hash[key] = val 111 | end 112 | 113 | hash 114 | end 115 | 116 | props = munge(pairs) 117 | 118 | # TODO: remove duct tape for #13 119 | # 120 | # The :family property is making less and less sense because it seems that 121 | # ipv6 configuration should add new properties instead of trying to collide 122 | # with the ipv4 addresses. But right now, the :inet property is never used 123 | # and it's creating a change on each resource update. This is a patch until 124 | # the :family property is ripped out. 125 | # 126 | # See https://github.com/adrienthebo/puppet-network/issues/13 for the full 127 | # issue that caused this, and https://github.com/adrienthebo/puppet-network/issues/16 128 | # for the resolution. 129 | # 130 | props[:family] = :inet 131 | 132 | # If there is no DEVICE property in the interface configuration we retrieve 133 | # the interface name from the file name itself 134 | props[:name] = filename.split('ifcfg-')[1] unless props.key?(:name) 135 | 136 | # Set the field to the default to prevent changes to the resource 137 | props[:onboot] = true 138 | 139 | # The FileMapper mixin expects an array of providers, so we return the 140 | # single interface wrapped in an array 141 | [props] 142 | end 143 | 144 | # @api private 145 | def self.munge(pairs) 146 | props = {} 147 | 148 | # Unquote all values 149 | pairs.each_pair do |key, val| 150 | if (munged = val.gsub(%r{['"]}, '')) 151 | pairs[key] = munged 152 | end 153 | end 154 | 155 | # For each interface attribute that we recognize it, add the value to the 156 | # hash with our expected label 157 | SLES_NAME_MAPPINGS.each_pair do |type_name, sles_name| 158 | next unless (val = pairs[sles_name]) 159 | 160 | pairs.delete(sles_name) 161 | props[type_name] = val 162 | end 163 | 164 | # if we encounter VLAN_ID parameter, set the interface mode to :vlan 165 | pairs.each_pair do |key, _val| 166 | props[:mode] = :vlan if key == 'VLAN_ID' 167 | end 168 | pairs.delete('VLAN_ID') 169 | 170 | # mode is a property so it should always have a value 171 | props[:mode] ||= :raw 172 | 173 | # For all of the remaining values, blindly toss them into the options hash. 174 | props[:options] = pairs 175 | 176 | props[:method] = 'static' unless %w[none bootp dhcp].include? props[:method] 177 | 178 | props 179 | end 180 | 181 | def self.format_file(filename, providers) 182 | return '' if providers.empty? 183 | 184 | if providers.length > 1 185 | raise Puppet::DevError, 186 | "Unable to support multiple interfaces [#{providers.map(&:name).join(',')}] in a single file #{filename}" 187 | end 188 | 189 | provider = providers[0] 190 | props = {} 191 | 192 | props = provider.options if provider.options && provider.options != :absent 193 | 194 | # Map everything to a flat hash 195 | SLES_NAME_MAPPINGS.each_key do |type_name| 196 | if (val = provider.send(type_name)) && val != :absent 197 | props[type_name] = val 198 | end 199 | end 200 | 201 | # :mode does not exist in SLES_NAME_MAPPINGS so we have to fetch it manually 202 | # note that the inverse operation is in .munge instead of parse_file 203 | val = provider.send(:mode) 204 | props['VLAN_ID'] = provider.options['VLAN_ID'] if val == :vlan 205 | 206 | pairs = unmunge props 207 | 208 | pairs.each_with_object('') do |(key, value), str| 209 | str << %(#{key}=#{value}\n) 210 | end 211 | end 212 | 213 | def self.unmunge(props) 214 | pairs = {} 215 | 216 | SLES_NAME_MAPPINGS.each_pair do |type_name, sles_name| 217 | if (val = props[type_name]) 218 | props.delete(type_name) 219 | pairs[sles_name] = val 220 | end 221 | end 222 | 223 | pairs.merge! props 224 | 225 | pairs.each_pair do |key, val| 226 | pairs[key] = %("#{val}") if val.is_a?(String) && val.match(%r{\s+}) 227 | end 228 | 229 | pairs 230 | end 231 | 232 | def self.post_flush_hook(filename) 233 | File.chmod(0o644, filename) 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /lib/puppet/provider/network_config/redhat.rb: -------------------------------------------------------------------------------- 1 | require 'puppetx/filemapper' 2 | 3 | Puppet::Type.type(:network_config).provide(:redhat) do 4 | # Red Hat network_config network scripts provider. 5 | # 6 | # This provider manages the contents of /etc/networks-scripts/ifcfg-* to 7 | # manage non-volatile network configuration. 8 | # 9 | # @see https://access.redhat.com/knowledge/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/s1-networkscripts-interfaces.html "Red Hat Interface Configuration Files" 10 | 11 | include PuppetX::FileMapper 12 | 13 | desc 'Redhat network-scripts provider' 14 | 15 | confine 'os.family' => :redhat 16 | defaultfor 'os.family' => :redhat 17 | 18 | has_feature :hotpluggable 19 | has_feature :provider_options 20 | 21 | # @return [String] The path to network-script directory on redhat systems 22 | SCRIPT_DIRECTORY = '/etc/sysconfig/network-scripts'.freeze 23 | 24 | # The valid vlan ID range is 0-4095; 4096 is out of range 25 | VLAN_RANGE_REGEX = %r{[1-3]?\d{1,3}|40[0-8]\d|409[0-5]} 26 | 27 | # aliases are almost free game, redhat rejects some, and max total length is 15 characters 28 | # 15 minus at least 2 for the interface name, and a colon leaves 12 characters for the alias 29 | ALIAS_REGEX = %r{.{1,12}(? 'eth1') 50 | # prov.select_file # => '/etc/sysconfig/network-scripts/ifcfg-eth1' 51 | # 52 | def select_file 53 | "#{SCRIPT_DIRECTORY}/ifcfg-#{name}" 54 | end 55 | 56 | # Scan all files in the networking directory for interfaces 57 | # 58 | # @param script_dir [String] The path to the networking scripts, defaults to 59 | # {#SCRIPT_DIRECTORY} 60 | # 61 | # @return [Array] All network-script config files on this machine. 62 | # 63 | # @example 64 | # RedhatProvider.target_files 65 | # # => ['/etc/sysconfig/network-scripts/ifcfg-eth0', '/etc/sysconfig/network-scripts/ifcfg-eth1'] 66 | def self.target_files(script_dir = SCRIPT_DIRECTORY) 67 | entries = Dir.entries(script_dir).select { |entry| entry.match SCRIPT_REGEX } 68 | entries.map { |entry| File.join(SCRIPT_DIRECTORY, entry) } 69 | end 70 | 71 | # Convert a redhat network script into a hash 72 | # 73 | # This is a hook method that will be called by PuppetX::Filemapper 74 | # 75 | # @param [String] filename The path of the interfaces file being parsed 76 | # @param [String] contents The contents of the given file 77 | # 78 | # @return [Array>] A single element array containing 79 | # the key/value pairs of properties parsed from the file. 80 | # 81 | # @example 82 | # RedhatProvider.parse_file('/etc/sysconfig/network-scripts/ifcfg-eth0', #) 83 | # # => [ 84 | # # { 85 | # # :name => 'eth0', 86 | # # :ipaddress => '169.254.0.1', 87 | # # :netmask => '255.255.0.0', 88 | # # }, 89 | # # ] 90 | def self.parse_file(filename, contents) 91 | # Split up the file into lines 92 | lines = contents.split("\n") 93 | # Strip out all comments 94 | lines.map! { |line| line.sub(%r{#.*$}, '') } 95 | # Remove all blank lines 96 | lines.reject! { |line| line =~ %r{^\s*$} } 97 | 98 | pair_regex = %r{^\s*(.+?)\s*=\s*(.*)\s*$} 99 | 100 | # INFO: It is valid ifcfg format to include certain functions and strings these should not be reported as errors, but silently removed before parsing 101 | # https://bugs.centos.org/bug_view_page.php?bug_id=475 102 | # Remove lines with no = sign 103 | lines.select! { |line| line =~ %r{^.*=.*$} } 104 | 105 | # Convert the data into key/value pairs 106 | pairs = lines.each_with_object({}) do |line, hash| 107 | raise Puppet::Error, %(#{filename} is malformed; "#{line}" did not match "#{pair_regex}") unless line.match(pair_regex) do |m| 108 | key = m[1].strip 109 | val = m[2].strip 110 | hash[key] = val 111 | end 112 | 113 | hash 114 | end 115 | 116 | props = munge(pairs) 117 | 118 | # TODO: remove duct tape for #13 119 | # 120 | # The :family property is making less and less sense because it seems that 121 | # ipv6 configuration should add new properties instead of trying to collide 122 | # with the ipv4 addresses. But right now, the :inet property is never used 123 | # and it's creating a change on each resource update. This is a patch until 124 | # the :family property is ripped out. 125 | # 126 | # See https://github.com/adrienthebo/puppet-network/issues/13 for the full 127 | # issue that caused this, and https://github.com/adrienthebo/puppet-network/issues/16 128 | # for the resolution. 129 | # 130 | props[:family] = :inet 131 | 132 | # If there is no DEVICE property in the interface configuration we retrieve 133 | # the interface name from the file name itself 134 | props[:name] = filename.split('ifcfg-')[1] unless props.key?(:name) 135 | 136 | # The FileMapper mixin expects an array of providers, so we return the 137 | # single interface wrapped in an array 138 | [props] 139 | end 140 | 141 | # @api private 142 | def self.munge(pairs) 143 | props = {} 144 | 145 | # Unquote all values 146 | pairs.each_pair do |key, val| 147 | if (munged = val.gsub(%r{['"]}, '')) 148 | pairs[key] = munged 149 | end 150 | end 151 | 152 | # For each interface attribute that we recognize it, add the value to the 153 | # hash with our expected label 154 | NAME_MAPPINGS.each_pair do |type_name, redhat_name| 155 | next unless (val = pairs[redhat_name]) 156 | 157 | pairs.delete(redhat_name) 158 | props[type_name] = val 159 | end 160 | 161 | # if we encounter VLAN=yes set the interface mode to :vlan 162 | pairs.each_pair do |key, val| 163 | props[:mode] = :vlan if key == 'VLAN' && val == 'yes' 164 | end 165 | pairs.delete('VLAN') 166 | 167 | # mode is a property so it should always have a value 168 | props[:mode] ||= :raw 169 | 170 | # For all of the remaining values, blindly toss them into the options hash. 171 | props[:options] = pairs 172 | 173 | %i[onboot hotplug].each do |bool_property| 174 | props[bool_property] = (props[bool_property] == 'yes') unless props[bool_property].nil? 175 | end 176 | 177 | props[:method] = 'static' unless %w[bootp dhcp].include? props[:method] 178 | 179 | props 180 | end 181 | 182 | def self.format_file(filename, providers) 183 | return '' if providers.empty? 184 | 185 | if providers.length > 1 186 | raise Puppet::DevError, 187 | "Unable to support multiple interfaces [#{providers.map(&:name).join(',')}] in a single file #{filename}" 188 | end 189 | 190 | provider = providers[0] 191 | props = {} 192 | 193 | props = provider.options if provider.options && provider.options != :absent 194 | 195 | # Map everything to a flat hash 196 | NAME_MAPPINGS.keys.each do |type_name| 197 | if (val = provider.send(type_name)) && val != :absent 198 | props[type_name] = val 199 | end 200 | end 201 | 202 | # :mode does not exist in NAME_MAPPINGS so we have to fetch it manually 203 | # note that the inverse operation is in .munge instead of parse_file 204 | val = provider.send(:mode) 205 | props['VLAN'] = 'yes' if val == :vlan 206 | 207 | pairs = unmunge props 208 | 209 | pairs.each_with_object('') do |(key, value), str| 210 | str << %(#{key}=#{value}\n) 211 | end 212 | end 213 | 214 | def self.unmunge(props) 215 | pairs = {} 216 | 217 | %i[onboot hotplug].each do |bool_property| 218 | unless props[bool_property].nil? 219 | props[bool_property] = (props[bool_property] == true ? 'yes' : 'no') 220 | end 221 | end 222 | 223 | NAME_MAPPINGS.each_pair do |type_name, redhat_name| 224 | if (val = props[type_name]) 225 | props.delete(type_name) 226 | pairs[redhat_name] = val 227 | end 228 | end 229 | 230 | pairs.merge! props 231 | 232 | pairs.each_pair do |key, val| 233 | pairs[key] = %("#{val}") if val.is_a?(String) && val.match(%r{\s+}) 234 | end 235 | 236 | pairs 237 | end 238 | 239 | def self.post_flush_hook(filename) 240 | File.chmod(0o644, filename) 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Adrien Thebo 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | -------------------------------------------------------------------------------- /spec/unit/type/network_config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Puppet::Type.type(:network_config) do 4 | before do 5 | provider_class = instance_double 'provider class' 6 | allow(provider_class).to receive(:name).and_return('fake') 7 | allow(provider_class).to receive(:suitable?).and_return(true) 8 | allow(provider_class).to receive(:supports_parameter?).and_return(true) 9 | allow(provider_class).to receive(:new) 10 | allow(Puppet::Type.type(:network_config)).to receive(:defaultprovider).and_return(provider_class) 11 | allow(Puppet::Type.type(:network_config)).to receive(:provider).and_return(provider_class) 12 | end 13 | 14 | describe 'feature' do 15 | describe 'hotpluggable' do 16 | it { expect(Puppet::Type.type(:network_config).provider_feature(:hotpluggable)).not_to be_nil } 17 | end 18 | 19 | describe 'reconfigurable' do 20 | it { expect(Puppet::Type.type(:network_config).provider_feature(:reconfigurable)).not_to be_nil } 21 | end 22 | 23 | describe 'provider_options' do 24 | it { expect(Puppet::Type.type(:network_config).provider_feature(:provider_options)).not_to be_nil } 25 | end 26 | end 27 | 28 | describe 'when validating the attribute' do 29 | describe :name do # rubocop:disable RSpec/DescribeSymbol 30 | it { expect(Puppet::Type.type(:network_config).attrtype(:name)).to eq(:param) } 31 | end 32 | 33 | describe :reconfigure do # rubocop:disable RSpec/DescribeSymbol 34 | it { expect(Puppet::Type.type(:network_config).attrtype(:reconfigure)).to eq(:param) } 35 | 36 | it 'requires the :reconfigurable parameter' do 37 | expect(Puppet::Type.type(:network_config).paramclass(:reconfigure).required_features).to include(:reconfigurable) 38 | end 39 | end 40 | 41 | %i[ensure ipaddress netmask method family onboot mtu mode options].each do |property| 42 | describe property do 43 | it { expect(Puppet::Type.type(:network_config).attrtype(property)).to eq(:property) } 44 | end 45 | end 46 | 47 | it 'use the name parameter as the namevar' do 48 | expect(Puppet::Type.type(:network_config).key_attributes).to eq([:name]) 49 | end 50 | 51 | describe 'ensure' do 52 | it 'is an ensurable value' do 53 | expect(Puppet::Type.type(:network_config).propertybyname(:ensure).ancestors).to include(Puppet::Property::Ensure) 54 | end 55 | end 56 | 57 | describe 'hotplug' do 58 | it 'requires the :hotpluggable feature' do 59 | expect(Puppet::Type.type(:network_config).propertybyname(:hotplug).required_features).to include(:hotpluggable) 60 | end 61 | end 62 | 63 | describe 'options' do 64 | it 'requires the :has_options feature' do 65 | expect(Puppet::Type.type(:network_config).propertybyname(:options).required_features).to include(:provider_options) 66 | end 67 | 68 | it 'is a descendant of the KeyValue property' do 69 | pending 'on conversion to specific type' 70 | expect(Puppet::Type.type(:network_config).propertybyname(:options).ancestors).to include(Puppet::Property::Ensure) 71 | end 72 | end 73 | end 74 | 75 | describe 'when validating the attribute value' do 76 | describe 'ipaddress' do 77 | let(:address4) { '127.0.0.1' } 78 | let(:address6) { '::1' } 79 | 80 | describe 'using the inet family' do 81 | it 'requires that a passed address is a valid IPv4 address' do 82 | expect { Puppet::Type.type(:network_config).new(name: 'yay', family: :inet, ipaddress: address4) }.not_to raise_error 83 | end 84 | 85 | it 'fails when passed an IPv6 address' do 86 | pending('Not yet implemented') 87 | expect { Puppet::Type.type(:network_config).new(name: 'yay', family: :inet, ipaddress: address6) }.to raise_error(%r{not a valid ipv4 address}) 88 | end 89 | end 90 | 91 | describe 'using the inet6 family' do 92 | it 'requires that a passed address is a valid IPv6 address' do 93 | expect { Puppet::Type.type(:network_config).new(name: 'yay', family: :inet6, ipaddress: address6) }.not_to raise_error 94 | end 95 | 96 | it 'fails when passed an IPv4 address' do 97 | pending('Not yet implemented') 98 | expect { Puppet::Type.type(:network_config).new(name: 'yay', family: :inet6, ipaddress: address4) }.to raise_error(%r{not a valid ipv6 address}) 99 | end 100 | end 101 | 102 | it 'fails if a malformed address is used' do 103 | expect { Puppet::Type.type(:network_config).new(name: 'yay', ipaddress: 'This is clearly not an IP address') }.to raise_error(%r{requires a valid ipaddress}) 104 | end 105 | end 106 | 107 | describe 'netmask' do 108 | it 'fails if an invalid CIDR netmask is used' do 109 | expect do 110 | Puppet::Type.type(:network_config).new(name: 'yay', netmask: 'This is clearly not a netmask') 111 | end.to raise_error(%r{requires a valid netmask}) 112 | end 113 | end 114 | 115 | describe 'method' do 116 | %w[static manual dhcp].each do |mth| 117 | it "considers '#{mth}' a valid configuration method" do 118 | Puppet::Type.type(:network_config).new(name: 'yay', method: mth) 119 | end 120 | end 121 | end 122 | 123 | describe 'family' do 124 | %w[inet inet6].each do |family| 125 | it "considers '#{family}' a valid address family" do 126 | Puppet::Type.type(:network_config).new(name: 'yay', family: family) 127 | end 128 | end 129 | end 130 | 131 | describe 'onboot' do 132 | [true, false].each do |bool| 133 | it "accepts '#{bool}' for onboot" do 134 | Puppet::Type.type(:network_config).new(name: 'yay', onboot: bool) 135 | end 136 | end 137 | end 138 | 139 | describe 'reconfigure' do 140 | [true, false].each do |bool| 141 | it "accepts '#{bool}' for reconfigure" do 142 | Puppet::Type.type(:network_config).new(name: 'yay', reconfigure: bool) 143 | end 144 | end 145 | end 146 | 147 | describe 'mtu' do 148 | it 'validates a tiny mtu size' do 149 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: '42') 150 | end 151 | 152 | it 'validates a tiny mtu size as a number' do 153 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: 42) 154 | end 155 | 156 | it 'validates a normal mtu size' do 157 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: '1500') 158 | end 159 | 160 | it 'validates a normal mtu size as a number' do 161 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: 1500) 162 | end 163 | 164 | it 'validates a large mtu size' do 165 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: '16384') 166 | end 167 | 168 | it 'validates a large mtu size as a number' do 169 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: 16_384) 170 | end 171 | 172 | it 'fails if an random string is passed' do 173 | expect do 174 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: 'This is clearly not a mtu') 175 | end.to raise_error(%r{must be a positive integer}) 176 | end 177 | 178 | it 'fails on values < 42' do 179 | expect do 180 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: '41') 181 | end.to raise_error(%r{is not in the valid mtu range}) 182 | end 183 | 184 | it 'fails on numeric values < 42' do 185 | expect do 186 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: 41) 187 | end.to raise_error(%r{is not in the valid mtu range}) 188 | end 189 | 190 | it 'fails on zero' do 191 | expect do 192 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: '0') 193 | end.to raise_error(%r{is not in the valid mtu range}) 194 | end 195 | 196 | it 'fails on numeric zero' do 197 | expect do 198 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: 0) 199 | end.to raise_error(%r{is not in the valid mtu range}) 200 | end 201 | 202 | it 'fails on values > 65536' do 203 | expect do 204 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: '65537') 205 | end.to raise_error(%r{is not in the valid mtu range}) 206 | end 207 | 208 | it 'fails on numeric values > 65536' do 209 | expect do 210 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: 65_537) 211 | end.to raise_error(%r{is not in the valid mtu range}) 212 | end 213 | 214 | it 'fails on negative values' do 215 | expect do 216 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: '-1500') 217 | end.to raise_error(%r{is not a valid mtu}) 218 | end 219 | 220 | it 'fails on negative numbers' do 221 | expect do 222 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: -1500) 223 | end.to raise_error(%r{is not in the valid mtu range}) 224 | end 225 | 226 | it 'fails on non-integer values' do 227 | expect do 228 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: '1500.1') 229 | end.to raise_error(%r{must be a positive integer}) 230 | end 231 | 232 | it 'fails on numeric non-integer values' do 233 | expect do 234 | Puppet::Type.type(:network_config).new(name: 'yay', mtu: 1500.1) 235 | end.to raise_error(%r{must be a positive integer}) 236 | end 237 | end 238 | 239 | describe 'mode' do 240 | %w[raw vlan].each do |value| 241 | it "accepts '#{value}' for interface mode" do 242 | Puppet::Type.type(:network_config).new(name: 'yay', mode: value) 243 | end 244 | end 245 | it 'fails on undefined values' do 246 | expect do 247 | Puppet::Type.type(:network_config).new(name: 'yay', mode: 'foo') 248 | end.to raise_error(%r{Invalid value "foo". Valid values are}) 249 | end 250 | 251 | it 'defaults to :raw' do 252 | expect(Puppet::Type.type(:network_config).new(name: 'yay')[:mode]).to eq(:raw) 253 | end 254 | end 255 | 256 | describe 'options' do 257 | it 'accepts an empty hash' do 258 | expect do 259 | Puppet::Type.type(:network_config).new(name: 'valid', options: {}) 260 | end.not_to raise_error 261 | end 262 | 263 | it 'uses an empty hash as the default' do 264 | expect do 265 | Puppet::Type.type(:network_config).new(name: 'valid') 266 | end.not_to raise_error 267 | end 268 | 269 | it 'fails if a non-hash is passed' do 270 | expect do 271 | Puppet::Type.type(:network_config).new(name: 'valid', options: 'geese') 272 | end.to raise_error(%r{requires a hash for the 'options' parameter}) 273 | end 274 | end 275 | end 276 | end 277 | -------------------------------------------------------------------------------- /lib/puppet/provider/network_config/interfaces.rb: -------------------------------------------------------------------------------- 1 | require 'puppetx/filemapper' 2 | 3 | Puppet::Type.type(:network_config).provide(:interfaces) do 4 | # Debian network_config interfaces provider. 5 | # 6 | # This provider uses the filemapper mixin to map the interfaces file to a 7 | # collection of network_config providers, and back. 8 | # 9 | # @see http://wiki.debian.org/NetworkConfiguration 10 | # @see http://packages.debian.org/squeeze/ifupdown 11 | 12 | include PuppetX::FileMapper 13 | 14 | desc 'Debian interfaces style provider' 15 | 16 | confine 'os.family' => :debian 17 | defaultfor 'os.family' => :debian 18 | 19 | has_feature :provider_options 20 | has_feature :hotpluggable 21 | 22 | def select_file 23 | '/etc/network/interfaces' 24 | end 25 | 26 | def self.target_files 27 | ['/etc/network/interfaces'] 28 | end 29 | 30 | class MalformedInterfacesError < Puppet::Error 31 | def initialize(msg = nil) 32 | msg ||= 'Malformed debian interfaces file; cannot instantiate network_config resources' 33 | super 34 | end 35 | end 36 | 37 | def self.raise_malformed 38 | @failed = true 39 | raise MalformedInterfacesError 40 | end 41 | 42 | class Instance 43 | attr_reader :name 44 | 45 | # Booleans 46 | attr_accessor :onboot, :hotplug 47 | 48 | # These fields are going to get rearranged to resolve issue 16 49 | # https://github.com/adrienthebo/puppet-network/issues/16 50 | attr_accessor :ipaddress, :netmask, :family, :method, :mtu, :mode 51 | 52 | # Options hash 53 | attr_reader :options 54 | 55 | def initialize(name) 56 | @name = name 57 | 58 | @options = Hash.new { |hash, key| hash[key] = [] } 59 | end 60 | 61 | def to_hash 62 | h = { 63 | name: @name, 64 | onboot: @onboot, 65 | hotplug: @hotplug, 66 | ipaddress: @ipaddress, 67 | netmask: @netmask, 68 | family: @family, 69 | method: @method, 70 | mtu: @mtu, 71 | mode: @mode, 72 | options: squeeze_options 73 | } 74 | 75 | h.each_with_object({}) do |(key, val), hash| 76 | hash[key] = val if val 77 | end 78 | end 79 | 80 | def squeeze_options 81 | @options.each_with_object({}) do |(key, value), hash| 82 | hash[key] = if value.size <= 1 83 | value.pop 84 | else 85 | value 86 | end 87 | hash 88 | end 89 | end 90 | 91 | class << self 92 | def reset! 93 | @interfaces = {} 94 | end 95 | 96 | # @return [Array] All class instances 97 | def all_instances 98 | @interfaces ||= {} 99 | @interfaces 100 | end 101 | 102 | def [](name) 103 | if all_instances[name] 104 | obj = all_instances[name] 105 | else 106 | obj = new(name) 107 | all_instances[name] = obj 108 | end 109 | 110 | obj 111 | end 112 | end 113 | end 114 | 115 | def self.parse_file(_filename, contents) 116 | # Debian has a very irregular format for the interfaces file. The 117 | # parse_file method is somewhat derived from the ifup executable 118 | # supplied in the debian ifupdown package. The source can be found at 119 | # http://packages.debian.org/squeeze/ifupdown 120 | 121 | # The debian interfaces implementation requires global state while parsing 122 | # the file; namely, the stanza being parsed as well as the interface being 123 | # parsed. 124 | status = :none 125 | current_interface = nil 126 | lines = contents.split("\n") 127 | # TODO: Join lines that end with a backslash 128 | 129 | # Iterate over all lines and determine what attributes they create 130 | lines.each do |line| 131 | # Strip off any trailing comments 132 | line.sub!(%r{#.*$}, '') 133 | 134 | case line 135 | when %r{^\s*#|^\s*$} 136 | # Ignore comments and blank lines 137 | next 138 | 139 | when %r{^source|^source-directory} 140 | # ignore source|source-directory sections, it makes this provider basically useless 141 | # with Debian Jessie. Please refer to man 5 interfaces 142 | next 143 | 144 | when %r{^auto|^allow-auto} 145 | # Parse out any auto sections 146 | interfaces = line.split(' ') 147 | interfaces.delete_at(0) 148 | 149 | interfaces.each do |name| 150 | Instance[name].onboot = true 151 | end 152 | 153 | # Reset the current parse state 154 | current_interface = nil 155 | 156 | when %r{^allow-hotplug} 157 | # parse out allow-hotplug lines 158 | 159 | interfaces = line.split(' ') 160 | interfaces.delete_at(0) 161 | 162 | interfaces.each do |name| 163 | Instance[name].hotplug = true 164 | end 165 | 166 | # Don't reset Reset the current parse state 167 | when %r{^iface} 168 | 169 | # Format of the iface line: 170 | # 171 | # iface 172 | # zero or more options for 173 | 174 | raise_malformed unless line.match(%r{^iface\s+(\S+)\s+(\S+)\s+(\S+)}) do |matched| 175 | name = matched[1] 176 | family = matched[2] 177 | method = matched[3] 178 | 179 | # If an iface block for this interface has been seen, the file is 180 | # malformed. 181 | raise_malformed if Instance[name] && Instance[name].family 182 | 183 | status = :iface 184 | current_interface = name 185 | 186 | # This is done automatically 187 | # Instance[name].name = name 188 | Instance[name].family = family 189 | Instance[name].method = method 190 | # for the vlan naming conventions (a mess), see 191 | # man 5 vlan-interfaces 192 | Instance[name].mode = case name 193 | # 'vlan22' 194 | when %r{^vlan} 195 | :vlan 196 | # 'eth2.0003' or 'br1.2' 197 | when %r{^[a-z]{1,}[0-9]{1,}\.[0-9]{1,4}} 198 | :vlan 199 | else 200 | :raw 201 | end 202 | end 203 | 204 | when %r{^mapping} 205 | # XXX dox 206 | status = :mapping 207 | raise Puppet::DevError, 'Debian interfaces mapping parsing not implemented.' 208 | 209 | else 210 | # We're currently examining a line that is within a mapping or iface 211 | # stanza, so we need to validate the line and add the options it 212 | # specifies to the known state of the interface. 213 | 214 | case status 215 | when :iface 216 | raise_malformed unless line.match(%r{(\S+)\s+(\S.*)}) do |matched| 217 | # If we're parsing an iface stanza, then we should receive a set of 218 | # lines that contain two or more space delimited strings. Append 219 | # them as options to the iface in an array. 220 | 221 | key = matched[1] 222 | val = matched[2] 223 | 224 | name = current_interface 225 | 226 | case key # rubocop:disable Metrics/BlockNesting 227 | when 'address' then Instance[name].ipaddress = val 228 | when 'netmask' then Instance[name].netmask = val 229 | when 'mtu' then Instance[name].mtu = val 230 | else Instance[name].options[key] << val 231 | end 232 | end 233 | when :mapping 234 | raise Puppet::DevError, 'Debian interfaces mapping parsing not implemented.' 235 | when :none 236 | raise_malformed 237 | end 238 | end 239 | end 240 | Instance.all_instances.map { |_name, instance| instance.to_hash } 241 | end 242 | 243 | # Generate an array of sections 244 | def self.format_file(_filename, providers) 245 | contents = [] 246 | contents << header 247 | 248 | # Add onboot interfaces 249 | auto_interfaces = providers.select { |provider| provider.onboot == true } 250 | unless auto_interfaces.empty? 251 | stanza = [] 252 | stanza << ('auto ' + auto_interfaces.map(&:name).sort.join(' ')) 253 | contents << stanza.join("\n") 254 | end 255 | 256 | # Add hotpluggable interfaces 257 | hotplug_interfaces = providers.select { |provider| provider.hotplug == true } 258 | unless hotplug_interfaces.empty? 259 | stanza = [] 260 | stanza << ('allow-hotplug ' + hotplug_interfaces.map(&:name).sort.join(' ')) 261 | contents << stanza.join("\n") 262 | end 263 | 264 | # Build iface stanzas 265 | providers.sort_by(&:name).each do |provider| 266 | # TODO: add validation method 267 | raise Puppet::Error, "#{provider.name} does not have a method." if provider.method.nil? 268 | raise Puppet::Error, "#{provider.name} does not have a family." if provider.family.nil? 269 | 270 | stanza = [] 271 | stanza << %(iface #{provider.name} #{provider.family} #{provider.method}) 272 | 273 | # add the vlan-raw-device only when explicitely set in interfaces(5) stanza 274 | # otherwise it's already assumed given the stanza name (ethX.NN) 275 | if provider.mode == :vlan 276 | if provider.options['vlan-raw-device'] 277 | stanza << "vlan-raw-device #{provider.options['vlan-raw-device']}" 278 | else 279 | vlan_range_regex = %r{[1-3]?\d{1,3}|40[0-8]\d|409[0-5]} 280 | raise Puppet::Error, "Interface #{provider.name}: missing vlan-raw-device or wrong VLAN ID in the iface name " unless provider.name =~ %r{\A([a-z]+\d+)(?::\d+|\.#{vlan_range_regex})\Z} 281 | end 282 | end 283 | 284 | [ 285 | [:ipaddress, 'address'], 286 | [:netmask, 'netmask'], 287 | [:mtu, 'mtu'] 288 | ].each do |(property, section)| 289 | stanza << "#{section} #{provider.send property}" if provider.send(property) && provider.send(property) != :absent 290 | end 291 | 292 | if provider.options && provider.options != :absent 293 | provider.options.each_pair do |key, val| 294 | if val.is_a? String 295 | # dont print property because we've already added it to the stanza 296 | stanza << " #{key} #{val}" if key != 'vlan-raw-device' 297 | elsif val.is_a? Array 298 | val.each { |entry| stanza << " #{key} #{entry}" } 299 | else 300 | raise Puppet::Error, "#{self} options key #{key} expects a String or Array, got #{val.class}" 301 | end 302 | end 303 | end 304 | 305 | contents << stanza.join("\n") 306 | end 307 | 308 | contents.map { |line| line + "\n\n" }.join 309 | end 310 | 311 | def self.header 312 | <<~HEADER 313 | # HEADER: This file is being managed by puppet. Changes to 314 | # HEADER: interfaces that are not being managed by puppet will persist; 315 | # HEADER: however changes to interfaces that are being managed by puppet will 316 | # HEADER: be overwritten. In addition, file order is NOT guaranteed. 317 | # HEADER: Last generated at: #{Time.now} 318 | HEADER 319 | end 320 | end 321 | -------------------------------------------------------------------------------- /spec/unit/provider/network_config/sles_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rspec/its' 3 | 4 | describe Puppet::Type.type(:network_config).provider(:sles) do 5 | subject { described_class } 6 | 7 | def fixture_path 8 | File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_config', 'sles_spec') 9 | end 10 | 11 | def fixture_file(file) 12 | File.join(fixture_path, file) 13 | end 14 | 15 | def fixture_data(file) 16 | File.read(fixture_file(file)) 17 | end 18 | 19 | describe 'provider features' do 20 | it 'is not hotpluggable' do 21 | expect(described_class.declared_feature?(:hotpluggable)).to be false 22 | end 23 | 24 | it 'is startmode' do 25 | expect(described_class.declared_feature?(:startmode)).to be true 26 | end 27 | end 28 | 29 | describe 'selecting files to parse' do 30 | subject { described_class.target_files(network_scripts_path).map { |file| File.basename(file) } } 31 | 32 | let(:network_scripts_path) { fixture_file('network-scripts') } 33 | 34 | valid_files = %w[ifcfg-bond0 ifcfg-bond1 ifcfg-eth0 ifcfg-eth1 ifcfg-eth2 35 | ifcfg-eth3 ifcfg-vlan100 ifcfg-vlan200 36 | ifcfg-eth0.4095 ifcfg-bond1.1001] 37 | 38 | invalid_files = %w[.ifcfg-bond0.swp ifcfg-bond1~ ifcfg-vlan500.bak 39 | ifcfg-eth0:my.alias.bak ifcfg-eth0.4096] 40 | 41 | valid_files.each do |file| 42 | it { is_expected.to include file } 43 | end 44 | 45 | invalid_files.each do |file| 46 | it { is_expected.not_to include file } 47 | end 48 | end 49 | 50 | describe 'when parsing' do 51 | describe 'the name' do 52 | let(:data) { described_class.parse_file('ifcfg-eth0', fixture_data('ifcfg-eth0-dhcp'))[0] } 53 | 54 | it { expect(data[:name]).to eq('eth0') } 55 | end 56 | 57 | describe 'the startmode property' do 58 | let(:data) { described_class.parse_file('ifcfg-eth0', fixture_data('ifcfg-eth0-dhcp'))[0] } 59 | 60 | it { expect(data[:startmode]).to eq('auto') } 61 | end 62 | 63 | describe 'the bootproto property' do 64 | describe 'when dhcp' do 65 | let(:data) { described_class.parse_file('ifcfg-eth0', fixture_data('ifcfg-eth0-dhcp'))[0] } 66 | 67 | it { expect(data[:method]).to eq('dhcp') } 68 | end 69 | 70 | describe 'when static' do 71 | let(:data) { described_class.parse_file('ifcfg-eth0', fixture_data('ifcfg-eth0-static'))[0] } 72 | 73 | it { expect(data[:method]).to eq('static') } 74 | end 75 | end 76 | 77 | describe 'a static interface' do 78 | let(:data) { described_class.parse_file('eth0', fixture_data('ifcfg-eth0-static'))[0] } 79 | 80 | it { expect(data[:ipaddress]).to eq('10.0.1.27') } 81 | it { expect(data[:netmask]).to eq('255.255.255.0') } 82 | end 83 | 84 | describe 'the options property' do 85 | let(:data) { described_class.parse_file('eth0', fixture_data('ifcfg-eth0-static'))[0] } 86 | 87 | it { expect(data[:options]['LLADDR']).to eq('aa:bb:cc:dd:ee:ff') } 88 | end 89 | 90 | describe 'with no extra options' do 91 | let(:data) { described_class.parse_file('eth0', fixture_data('ifcfg-eth1-simple'))[0] } 92 | 93 | it { expect(data[:options]).to eq({}) } 94 | end 95 | 96 | describe 'complex configuration' do 97 | let(:virbonding_path) { File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_config', 'sles_spec', 'virbonding') } 98 | 99 | before do 100 | allow(described_class).to receive(:target_files).and_return(Dir["#{virbonding_path}/*"]) 101 | end 102 | 103 | describe 'bond0' do 104 | subject { described_class.instances.find { |i| i.name == 'bond0' } } 105 | 106 | its(:startmode) { is_expected.to eq('auto') } 107 | its(:mtu) { is_expected.to eq('1500') } 108 | 109 | its(:options) do 110 | is_expected.to eq( 111 | 'BONDING_MASTER' => 'yes', 112 | 'BONDING_MODULE_OPTS' => %(mode=4 miimon=100 xmit_hash_policy=layer3+4), 113 | 'BONDING_SLAVE_0' => 'eth0', 114 | 'BONDING_SLAVE_1' => 'eth1' 115 | ) 116 | end 117 | end 118 | 119 | describe 'bond1' do 120 | subject { described_class.instances.find { |i| i.name == 'bond1' } } 121 | 122 | its(:startmode) { is_expected.to eq('auto') } 123 | its(:ipaddress) { is_expected.to eq('172.20.1.9') } 124 | its(:netmask) { is_expected.to eq('255.255.255.0') } 125 | its(:mtu) { is_expected.to eq('1500') } 126 | 127 | its(:options) do 128 | is_expected.to eq( 129 | 'BONDING_MASTER' => 'yes', 130 | 'BONDING_MODULE_OPTS' => %(mode=4 miimon=100 xmit_hash_policy=layer3+4), 131 | 'BONDING_SLAVE_0' => 'eth2', 132 | 'BONDING_SLAVE_1' => 'eth3' 133 | ) 134 | end 135 | end 136 | 137 | describe 'eth0' do 138 | subject { described_class.instances.find { |i| i.name == 'eth0' } } 139 | 140 | its(:startmode) { is_expected.to eq('hotplug') } 141 | its(:mtu) { is_expected.to eq('1500') } 142 | its(:mode) { is_expected.to eq(:raw) } 143 | 144 | its(:options) do 145 | is_expected.to eq( 146 | 'LLADDR' => '00:12:79:91:28:1f' 147 | ) 148 | end 149 | end 150 | 151 | describe 'eth1' do 152 | subject { described_class.instances.find { |i| i.name == 'eth1' } } 153 | 154 | its(:startmode) { is_expected.to eq('hotplug') } 155 | its(:mtu) { is_expected.to eq('1500') } 156 | its(:mode) { is_expected.to eq(:raw) } 157 | 158 | its(:options) do 159 | is_expected.to eq( 160 | 'LLADDR' => '00:12:79:91:28:20' 161 | ) 162 | end 163 | end 164 | 165 | describe 'eth2' do 166 | subject { described_class.instances.find { |i| i.name == 'eth2' } } 167 | 168 | its(:startmode) { is_expected.to eq('hotplug') } 169 | its(:mtu) { is_expected.to eq('1500') } 170 | its(:mode) { is_expected.to eq(:raw) } 171 | 172 | its(:options) do 173 | is_expected.to eq( 174 | 'LLADDR' => '00:26:55:e9:33:c4' 175 | ) 176 | end 177 | end 178 | 179 | describe 'eth3' do 180 | subject { described_class.instances.find { |i| i.name == 'eth3' } } 181 | 182 | its(:startmode) { is_expected.to eq('hotplug') } 183 | its(:mtu) { is_expected.to eq('1500') } 184 | its(:mode) { is_expected.to eq(:raw) } 185 | 186 | its(:options) do 187 | is_expected.to eq( 188 | 'LLADDR' => '00:26:55:e9:33:c5' 189 | ) 190 | end 191 | end 192 | 193 | describe 'vlan100' do 194 | subject { described_class.instances.find { |i| i.name == 'vlan100' } } 195 | 196 | its(:ipaddress) { is_expected.to eq('172.24.61.11') } 197 | its(:netmask) { is_expected.to eq('255.255.255.0') } 198 | its(:startmode) { is_expected.to eq('off') } 199 | its(:method) { is_expected.to eq('static') } 200 | its(:mode) { is_expected.to eq(:vlan) } 201 | 202 | its(:options) do 203 | is_expected.to eq( 204 | 'ETHERDEVICE' => 'bond0' 205 | ) 206 | end 207 | end 208 | 209 | describe 'vlan200' do 210 | subject { described_class.instances.find { |i| i.name == 'vlan200' } } 211 | 212 | its(:ipaddress) { is_expected.to eq('172.24.62.1') } 213 | its(:netmask) { is_expected.to eq('255.255.255.0') } 214 | its(:startmode) { is_expected.to eq('off') } 215 | its(:method) { is_expected.to eq('static') } 216 | its(:mode) { is_expected.to eq(:vlan) } 217 | 218 | its(:options) do 219 | is_expected.to eq( 220 | 'ETHERDEVICE' => 'bond0' 221 | ) 222 | end 223 | end 224 | end 225 | 226 | describe 'interface.vlan_id vlan configuration' do 227 | let(:network_scripts_path) { fixture_file('network-scripts') } 228 | 229 | before do 230 | allow(described_class).to receive(:target_files).and_return(Dir["#{network_scripts_path}/*"]) 231 | end 232 | 233 | describe 'eth0.4095' do 234 | subject { described_class.instances.find { |i| i.name == 'eth0.4095' } } 235 | 236 | its(:startmode) { is_expected.to eq('auto') } 237 | its(:method) { is_expected.to eq('static') } 238 | its(:mtu) { is_expected.to eq('9000') } 239 | its(:mode) { is_expected.to eq(:vlan) } 240 | 241 | its(:options) do 242 | is_expected.to eq( 243 | 'INTERFACETYPE' => 'Ethernet', 244 | 'ETHERDEVICE' => 'br4095' 245 | ) 246 | end 247 | end 248 | end 249 | 250 | describe 'when DEVICE is not present' do 251 | let(:data) { described_class.parse_file('ifcfg-eth1', fixture_data('ifcfg-eth1-dhcp'))[0] } 252 | 253 | it { expect(data[:name]).to eq('eth1') } 254 | end 255 | end 256 | 257 | describe 'when formatting resources' do 258 | let(:eth0_provider) do 259 | instance_double('eth0_provider', 260 | name: 'eth0', 261 | ensure: :present, 262 | startmode: 'auto', 263 | hotplug: true, 264 | family: 'inet', 265 | method: 'none', 266 | ipaddress: '169.254.0.1', 267 | netmask: '255.255.255.0', 268 | mtu: '1500', 269 | mode: nil, 270 | options: {}) 271 | end 272 | 273 | let(:eth0_1_provider) do 274 | instance_double('eth0_1_provider', 275 | name: 'eth0.1', 276 | ensure: :present, 277 | startmode: 'auto', 278 | family: 'inet', 279 | method: 'none', 280 | ipaddress: '169.254.0.1', 281 | netmask: '255.255.255.0', 282 | mtu: '1500', 283 | mode: :vlan, 284 | options: {}) 285 | end 286 | 287 | let(:eth1_provider) do 288 | instance_double('eth1_provider', 289 | name: 'eth1', 290 | etherdevice: 'eth1', 291 | ensure: :present, 292 | startmode: 'off', 293 | family: 'inet', 294 | method: 'none', 295 | ipaddress: :absent, 296 | netmask: :absent, 297 | mtu: :absent, 298 | mode: :vlan, 299 | options: { 300 | 'ETHERDEVICE' => 'eth1' 301 | }) 302 | end 303 | 304 | let(:lo_provider) do 305 | instance_double('lo_provider', 306 | name: 'lo', 307 | startmode: 'yes', 308 | family: 'inet', 309 | method: 'loopback', 310 | ipaddress: nil, 311 | netmask: nil, 312 | mode: nil, 313 | options: {}) 314 | end 315 | 316 | let(:bond0_provider) do 317 | instance_double('bond0_provider', 318 | name: 'bond0', 319 | startmode: 'auto', 320 | hotplug: true, 321 | ipaddress: '172.20.1.9', 322 | netmask: '255.255.255.0', 323 | method: 'static', 324 | mtu: '1500', 325 | mode: nil, 326 | options: { 327 | 'BONDING_OPTS' => %(mode=4 miimon=100 xmit_hash_policy=layer3+4) 328 | }) 329 | end 330 | 331 | it 'fails if multiple interfaces are flushed to one file' do 332 | expect { described_class.format_file('filepath', [eth0_provider, lo_provider]) }.to raise_error Puppet::DevError, %r{multiple interfaces} 333 | end 334 | 335 | describe 'with test interface eth0' do 336 | let(:data) { described_class.format_file('filepath', [eth0_provider]) } 337 | 338 | it { expect(data).to match(%r{STARTMODE=auto}) } 339 | it { expect(data).to match(%r{BOOTPROTO=none}) } 340 | it { expect(data).to match(%r{IPADDR=169\.254\.0\.1}) } 341 | it { expect(data).to match(%r{NETMASK=255\.255\.255\.0}) } 342 | end 343 | 344 | describe 'with test interface eth0.1' do 345 | let(:data) { described_class.format_file('filepath', [eth0_1_provider]) } 346 | 347 | it { expect(data).to match(%r{STARTMODE=auto}) } 348 | it { expect(data).to match(%r{BOOTPROTO=none}) } 349 | it { expect(data).to match(%r{IPADDR=169\.254\.0\.1}) } 350 | it { expect(data).to match(%r{NETMASK=255\.255\.255\.0}) } 351 | end 352 | 353 | describe 'with test interface eth1' do 354 | let(:data) { described_class.format_file('filepath', [eth1_provider]) } 355 | 356 | it { expect(data).to match(%r{ETHERDEVICE=eth1}) } 357 | it { expect(data).to match(%r{BOOTPROTO=none}) } 358 | it { expect(data).to match(%r{STARTMODE=off}) } 359 | it { expect(data).not_to match(%r{absent}) } 360 | end 361 | 362 | describe 'with test interface bond0' do 363 | let(:data) { described_class.format_file('filepath', [bond0_provider]) } 364 | 365 | it { expect(data).to match(%r{BONDING_OPTS="mode=4 miimon=100 xmit_hash_policy=layer3\+4"}) } 366 | end 367 | end 368 | 369 | describe 'when flushing a dirty file' do 370 | before do 371 | allow(File).to receive(:chmod).with(0o644, '/not/a/real/file') 372 | allow(File).to receive(:unlink) 373 | allow(described_class).to receive(:perform_write) 374 | end 375 | 376 | it do 377 | described_class.dirty_file!('/not/a/real/file') 378 | described_class.flush_file('/not/a/real/file') 379 | end 380 | 381 | it 'is expected that it shouldnot have have unlinked the file' do 382 | expect(File).not_to have_received(:unlink) 383 | end 384 | end 385 | end 386 | -------------------------------------------------------------------------------- /spec/unit/provider/network_config/interfaces_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Puppet::Type.type(:network_config).provider(:interfaces) do 4 | def fixture_data(file) 5 | basedir = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_config', 'interfaces_spec') 6 | File.read(File.join(basedir, file)) 7 | end 8 | 9 | after do 10 | v_level = $VERBOSE 11 | $VERBOSE = nil 12 | Instance.reset! 13 | $VERBOSE = v_level 14 | end 15 | 16 | describe 'provider features' do 17 | it 'is hotpluggable' do 18 | expect(described_class.declared_feature?(:hotpluggable)).to be true 19 | end 20 | end 21 | 22 | describe 'when parsing' do 23 | it 'parses out auto interfaces' do 24 | fixture = fixture_data('loopback') 25 | data = described_class.parse_file('', fixture) 26 | expect(data.find { |h| h[:name] == 'lo' }[:onboot]).to be true 27 | end 28 | 29 | it "parses out allow-hotplug interfaces as 'hotplug'" do 30 | fixture = fixture_data('single_interface_dhcp') 31 | data = described_class.parse_file('', fixture) 32 | expect(data.find { |h| h[:name] == 'eth0' }[:hotplug]).to be true 33 | end 34 | 35 | it "parses out allow-auto interfaces as 'onboot'" do 36 | fixture = fixture_data('two_interface_dhcp') 37 | data = described_class.parse_file('', fixture) 38 | expect(data.find { |h| h[:name] == 'eth1' }[:onboot]).to be true 39 | end 40 | 41 | it 'parses out iface lines' do 42 | fixture = fixture_data('single_interface_dhcp') 43 | data = described_class.parse_file('', fixture) 44 | expect(data.find { |h| h[:name] == 'eth0' }).to eq(family: 'inet', 45 | method: 'dhcp', 46 | mode: :raw, 47 | name: 'eth0', 48 | hotplug: true, 49 | options: {}) 50 | end 51 | 52 | it 'ignores source and source-directory lines' do 53 | fixture = fixture_data('jessie_source_stanza') 54 | data = described_class.parse_file('', fixture) 55 | expect(data.find { |h| h[:name] == 'eth0' }).to eq(family: 'inet', 56 | method: 'dhcp', 57 | mode: :raw, 58 | name: 'eth0', 59 | hotplug: true, 60 | options: {}) 61 | end 62 | 63 | it 'ignores variable whitespace in iface lines (network-#26)' do 64 | fixture = fixture_data('iface_whitespace') 65 | data = described_class.parse_file('', fixture) 66 | expect(data.find { |h| h[:name] == 'eth0' }).to eq(family: 'inet', 67 | method: 'dhcp', 68 | mode: :raw, 69 | name: 'eth0', 70 | hotplug: true, 71 | options: {}) 72 | end 73 | 74 | it 'parses out lines following iface lines' do 75 | fixture = fixture_data('single_interface_static') 76 | data = described_class.parse_file('', fixture) 77 | expect(data.find { |h| h[:name] == 'eth0' }).to eq(name: 'eth0', 78 | family: 'inet', 79 | method: 'static', 80 | mode: :raw, 81 | ipaddress: '192.168.0.2', 82 | netmask: '255.255.255.0', 83 | onboot: true, 84 | mtu: '1500', 85 | options: { 86 | 'broadcast' => '192.168.0.255', 87 | 'gateway' => '192.168.0.1' 88 | }) 89 | end 90 | 91 | # mapping sections aren't support, and might not ever be supported. 92 | # it "should parse out mapping lines" 93 | # it "should parse out lines following mapping lines" 94 | 95 | it 'allows for multiple options sections' do 96 | fixture = fixture_data('single_interface_options') 97 | data = described_class.parse_file('', fixture) 98 | expect(data.find { |h| h[:name] == 'eth0' }).to eq(name: 'eth0', 99 | family: 'inet', 100 | method: 'dhcp', 101 | mode: :raw, 102 | options: { 103 | 'pre-up' => '/bin/touch /tmp/eth0-up', 104 | 'post-down' => [ 105 | '/bin/touch /tmp/eth0-down1', 106 | '/bin/touch /tmp/eth0-down2' 107 | ] 108 | }) 109 | end 110 | 111 | it 'parses out vlan iface lines' do 112 | fixture = fixture_data('two_interfaces_static_vlan') 113 | data = described_class.parse_file('', fixture) 114 | expect(data.find { |h| h[:name] == 'eth0' }).to eq(name: 'eth0', 115 | family: 'inet', 116 | method: 'static', 117 | mode: :raw, 118 | ipaddress: '192.168.0.2', 119 | netmask: '255.255.255.0', 120 | onboot: true, 121 | mtu: '1500', 122 | options: { 123 | 'broadcast' => '192.168.0.255', 124 | 'gateway' => '192.168.0.1' 125 | }) 126 | expect(data.find { |h| h[:name] == 'eth0.1' }).to eq(name: 'eth0.1', 127 | family: 'inet', 128 | method: 'static', 129 | ipaddress: '172.16.0.2', 130 | netmask: '255.255.255.0', 131 | onboot: true, 132 | mtu: '1500', 133 | mode: :vlan, 134 | options: { 135 | 'broadcast' => '172.16.0.255', 136 | 'gateway' => '172.16.0.1', 137 | 'vlan-raw-device' => 'eth0' 138 | }) 139 | end 140 | 141 | describe 'when reading an invalid interfaces' do 142 | it 'with misplaced options should fail' do 143 | expect do 144 | described_class.parse_file('', "address 192.168.1.1\niface eth0 inet static\n") 145 | end.to raise_error(%r{Malformed debian interfaces file}) 146 | end 147 | 148 | it 'with an option without a value should fail' do 149 | expect do 150 | described_class.parse_file('', "iface eth0 inet manual\naddress") 151 | end.to raise_error(%r{Malformed debian interfaces file}) 152 | end 153 | end 154 | end 155 | 156 | describe 'when formatting' do 157 | let(:eth0_provider) do 158 | instance_double('eth0_provider', 159 | name: 'eth0', 160 | ensure: :present, 161 | onboot: true, 162 | hotplug: true, 163 | family: 'inet', 164 | method: 'static', 165 | ipaddress: '169.254.0.1', 166 | netmask: '255.255.0.0', 167 | mtu: '1500', 168 | mode: nil, 169 | options: nil) 170 | end 171 | 172 | let(:vlan20_provider) do 173 | instance_double('vlan20_provider', 174 | name: 'vlan20', 175 | ensure: :present, 176 | onboot: true, 177 | hotplug: true, 178 | family: 'inet', 179 | method: 'static', 180 | ipaddress: '169.254.0.1', 181 | netmask: '255.255.0.0', 182 | mtu: '1500', 183 | mode: :vlan, 184 | options: { 185 | 'vlan-raw-device' => 'eth1' 186 | }) 187 | end 188 | 189 | let(:vlan10_provider) do 190 | instance_double('vlan10_provider', 191 | name: 'vlan10', 192 | ensure: :present, 193 | onboot: true, 194 | hotplug: true, 195 | family: 'inet', 196 | method: 'dhcp', 197 | ipaddress: nil, 198 | netmask: nil, 199 | mtu: nil, 200 | mode: :vlan, 201 | options: {}) 202 | end 203 | 204 | let(:eth1_4500_provider) do 205 | instance_double('eth1_4500_provider', 206 | name: 'eth1.4500', 207 | ensure: :present, 208 | onboot: true, 209 | hotplug: true, 210 | family: 'inet', 211 | method: 'dhcp', 212 | ipaddress: nil, 213 | netmask: nil, 214 | mtu: nil, 215 | mode: :vlan, 216 | options: {}) 217 | end 218 | 219 | let(:eth1_provider) do 220 | instance_double('eth1_provider', 221 | name: 'eth1', 222 | ensure: :present, 223 | onboot: false, 224 | hotplug: true, 225 | family: 'inet', 226 | method: 'static', 227 | ipaddress: '169.254.0.1', 228 | netmask: '255.255.0.0', 229 | mtu: '576', 230 | mode: nil, 231 | options: { 232 | 'pre-up' => '/bin/touch /tmp/eth1-up', 233 | 'post-down' => [ 234 | '/bin/touch /tmp/eth1-down1', 235 | '/bin/touch /tmp/eth1-down2' 236 | ] 237 | }) 238 | end 239 | 240 | let(:lo_provider) do 241 | instance_double('lo_provider', 242 | name: 'lo', 243 | onboot: true, 244 | hotplug: false, 245 | family: 'inet', 246 | method: 'loopback', 247 | ipaddress: nil, 248 | netmask: nil, 249 | mtu: '65536', 250 | mode: nil, 251 | options: nil) 252 | end 253 | 254 | before do 255 | allow(described_class).to receive(:header).and_return "# HEADER: stubbed header\n" 256 | end 257 | 258 | let(:content) { described_class.format_file('', [lo_provider, eth0_provider, eth1_provider]) } # rubocop:disable RSpec/ScatteredLet 259 | 260 | describe 'writing the auto section' do 261 | it 'allows at most one section' do 262 | expect(content.scan(%r{^auto .+$}).length).to eq(1) 263 | end 264 | 265 | it 'has the correct interfaces appended' do 266 | expect(content.scan(%r{^auto .+$}).first).to match('auto eth0 lo') 267 | end 268 | end 269 | 270 | describe 'writing only the auto section' do 271 | let(:content) { described_class.format_file('', [lo_provider]) } 272 | 273 | it 'skips the allow-hotplug line' do 274 | expect(content.scan(%r{^allow-hotplug .*$}).length).to eq(0) 275 | end 276 | end 277 | 278 | describe 'writing the allow-hotplug section' do 279 | it 'allows at most one section' do 280 | expect(content.scan(%r{^allow-hotplug .+$}).length).to eq(1) 281 | end 282 | 283 | it 'has the correct interfaces appended' do 284 | expect(content.scan(%r{^allow-hotplug .+$}).first).to match('allow-hotplug eth0 eth1') 285 | end 286 | end 287 | 288 | describe 'writing only the allow-hotplug section' do 289 | let(:content) { described_class.format_file('', [eth1_provider]) } 290 | 291 | it 'skips the auto line' do 292 | expect(content.scan(%r{^auto .*$}).length).to eq(0) 293 | end 294 | end 295 | 296 | describe 'writing iface blocks' do 297 | let(:content) { described_class.format_file('', [lo_provider, eth0_provider]) } 298 | 299 | it 'produces an iface block for each interface' do 300 | expect(content.scan(%r{iface eth0 inet static}).length).to eq(1) 301 | end 302 | 303 | it 'adds all options following the iface block' do 304 | block = [ 305 | 'iface eth0 inet static', 306 | 'address 169.254.0.1', 307 | 'netmask 255.255.0.0', 308 | 'mtu 1500' 309 | ].join("\n") 310 | expect(content.split('\n').find { |line| line.match(%r{iface eth0}) }).to match(block) 311 | end 312 | 313 | it 'fails if the family property is not defined' do 314 | allow(lo_provider).to receive(:family).and_return(nil) 315 | expect { content }.to raise_exception(%r{does not have a family}) 316 | end 317 | 318 | it 'fails if the method property is not defined' do 319 | allow(lo_provider).to receive(:method).and_return(nil) 320 | expect { content }.to raise_exception(%r{does not have a method}) 321 | end 322 | end 323 | 324 | describe 'writing vlan iface blocks' do 325 | let(:content) { described_class.format_file('', [vlan20_provider]) } 326 | 327 | it 'adds all options following the iface block' do 328 | block = [ 329 | 'iface vlan20 inet static', 330 | 'vlan-raw-device eth1', 331 | 'address 169.254.0.1', 332 | 'netmask 255.255.0.0', 333 | 'mtu 1500' 334 | ].join("\n") 335 | expect(content.split('\n').find { |line| line.match(%r{iface vlan20}) }).to match(block) 336 | end 337 | end 338 | 339 | describe 'writing wrong vlan iface blocks' do 340 | let(:content) { described_class.format_file('', [eth1_4500_provider]) } 341 | 342 | it 'fails with wrong VLAN ID' do 343 | expect { content }.to raise_error(Puppet::Error, %r{Interface eth1.4500: missing vlan-raw-device or wrong VLAN ID in the iface name}) 344 | end 345 | end 346 | 347 | describe 'writing wrong vlanNN iface blocks' do 348 | let(:content) { described_class.format_file('', [vlan10_provider]) } 349 | 350 | it 'fails with missing vlan-raw-device' do 351 | expect { content }.to raise_error(Puppet::Error, %r{Interface vlan10: missing vlan-raw-device or wrong VLAN ID in the iface name}) 352 | end 353 | end 354 | 355 | describe 'writing the options section' do 356 | let(:content) { described_class.format_file('', [eth1_provider]) } 357 | 358 | describe 'with a string value' do 359 | it 'writes a single entry' do 360 | expect(content.scan(%r{pre-up .*$}).size).to eq(1) 361 | end 362 | 363 | it 'writes the value as an modified string' do 364 | expect(content.scan(%r{^\s*pre-up .*$}).first).to eq(' pre-up /bin/touch /tmp/eth1-up') 365 | end 366 | end 367 | 368 | describe 'with an array value' do 369 | it 'writes an entry per array value' do 370 | expect(content.scan(%r{post-down .*$}).size).to eq(2) 371 | end 372 | 373 | it 'writes the values in order' do 374 | expect(content.scan(%r{^\s*post-down .*$})[0]).to eq(' post-down /bin/touch /tmp/eth1-down1') 375 | expect(content.scan(%r{^\s*post-down .*$})[1]).to eq(' post-down /bin/touch /tmp/eth1-down2') 376 | end 377 | end 378 | end 379 | end 380 | end 381 | --------------------------------------------------------------------------------