diff --git a/README.markdown b/README.markdown index b44d8282..fe5f8eb4 100644 --- a/README.markdown +++ b/README.markdown @@ -1,13 +1,15 @@ puppet-network ============== -Manage non-volatile network configuration. +Manage non-volatile network and route configuration. Travis Test status: [![Build Status](https://travis-ci.org/adrienthebo/puppet-network.png?branch=master)](https://travis-ci.org/adrienthebo/puppet-network) Examples -------- +Interface configuration + network_config { 'eth0': ensure => 'present', family => 'inet', @@ -33,6 +35,15 @@ Examples onboot => 'true', } +Route configuration + + network_route { '172.17.67.0': + ensure => 'present', + gateway => '172.18.6.2', + interface => 'vlan200', + netmask => '255.255.255.0', + } + Create resources on the fly with the `puppet resource` command: root@debian-6:~# puppet resource network_config eth1 ensure=present family=inet method=static ipaddress=169.254.0.1 netmask=255.255.0.0 @@ -46,10 +57,20 @@ Create resources on the fly with the `puppet resource` command: onboot => 'true', } + # puppet resource network_route 23.23.42.0 ensure=present netmask=255.255.255.0 interface=eth0 gateway=192.168.1.1 + notice: /Network_route[23.23.42.0]/ensure: created + network_route { '23.23.42.0': + ensure => 'present', + gateway => '192.168.1.1', + interface => 'eth0', + netmask => '255.255.255.0', + } + Dependencies ------------ The debian interfaces provider requires the FileMapper mixin, available at https://github.com/adrienthebo/puppet-filemapper +The debian routes provider requires the package ifupdown-extras The network_config type requires the Boolean mixin, available at https://github.com/adrienthebo/puppet-boolean diff --git a/lib/puppet/provider/network_route/routes.rb b/lib/puppet/provider/network_route/routes.rb new file mode 100644 index 00000000..566188f3 --- /dev/null +++ b/lib/puppet/provider/network_route/routes.rb @@ -0,0 +1,111 @@ +require 'ipaddr' +require 'puppetx/filemapper' + +Puppet::Type.type(:network_route).provide(:routes) do + # Debian network_route routes provider. + # + # This provider uses the filemapper mixin to map the routes file to a + # collection of network_route providers, and back. + # + # @see http://wiki.debian.org/NetworkConfiguration + # @see http://packages.debian.org/squeeze/ifupdown-extras + + include PuppetX::FileMapper + + desc "Debian routes style provider" + + confine :osfamily => :debian + + # $ dpkg -S /etc/network/if-up.d/20static-routes + # ifupdown-extra: /etc/network/if-up.d/20static-routes + confine :exists => '/etc/network/if-up.d/20static-routes' + + defaultfor :osfamily => :debian + + has_feature :provider_options + + def select_file + '/etc/network/routes' + end + + def self.target_files + ['/etc/network/routes'] + end + + class MalformedRoutesError < Puppet::Error + def initialize(msg = nil) + msg = 'Malformed debian routes file; cannot instantiate network_route resources' if msg.nil? + super + end + end + + def self.raise_malformed + @failed = true + raise MalformedRoutesError + end + + def self.parse_file(filename, contents) + # Build out an empty hash for new routes for storing their configs. + route_hash = Hash.new do |hash, key| + hash[key] = {} + hash[key][:name] = key + hash[key] + end + + lines = contents.split("\n") + lines.each do |line| + # Strip off any trailing comments + line.sub!(/#.*$/, '') + + if line =~ /^\s*#|^\s*$/ + # Ignore comments and blank lines + next + end + + route = line.split + + if route.length < 4 + raise_malformed + end + + # use the CIDR version of the target as :name + cidr_target = "#{route[0]}/#{IPAddr.new(route[1]).to_i.to_s(2).count('1')}" + + route_hash[cidr_target][:network] = route[0] + route_hash[cidr_target][:netmask] = route[1] + route_hash[cidr_target][:gateway] = route[2] + route_hash[cidr_target][:interface] = route[3] + end + + route_hash.values + end + + # Generate an array of sections + def self.format_file(filename, providers) + contents = [] + contents << header + + # Build routes + providers.sort_by(&:name).each do |provider| + raise Puppet::Error, "#{provider.name} does not have a network." if provider.network.nil? + raise Puppet::Error, "#{provider.name} does not have a netmask." if provider.netmask.nil? + raise Puppet::Error, "#{provider.name} does not have a gateway." if provider.gateway.nil? + raise Puppet::Error, "#{provider.name} does not have an interface" if provider.interface.nil? + + contents << "#{provider.network} #{provider.netmask} #{provider.gateway} #{provider.interface}\n" + end + + contents.join + end + + def self.header + str = <<-HEADER +# HEADER: This file is is being managed by puppet. Changes to +# HEADER: routes that are not being managed by puppet will persist; +# HEADER: however changes to routes that are being managed by puppet will +# HEADER: be overwritten. In addition, file order is NOT guaranteed. +# HEADER: Last generated at: #{Time.now} +HEADER + str + end +end diff --git a/lib/puppet/type/network_route.rb b/lib/puppet/type/network_route.rb new file mode 100644 index 00000000..bd48a034 --- /dev/null +++ b/lib/puppet/type/network_route.rb @@ -0,0 +1,58 @@ +require 'ipaddr' + +Puppet::Type.newtype(:network_route) do + @doc = "Manage non-volatile route configuration information" + + ensurable + + newparam(:name) do + isnamevar + desc "The name of the network route" + end + + newproperty(:network) do + isrequired + desc "The target network address" + + validate do |value| + begin + t = IPAddr.new(value) + rescue ArgumentError + fail("Invalid value for network: #{value}") + end + end + end + + newproperty(:netmask) do + isrequired + desc "The subnet mask to apply to the route" + + validate do |value| + unless (value.length <= 2 or value =~ /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/) # yikes + fail("Invalid value for argument netmask: #{value}") + end + end + + munge do |value| + r = IPAddr.new('255.255.255.255').mask(value.strip.to_i).to_s + end + end + + newproperty(:gateway) do + isrequired + desc "The gateway to use for the route" + + validate do |value| + begin + t = IPAddr.new(value) + rescue ArgumentError + fail("Invalid value for gateway: #{value}") + end + end + end + + newproperty(:interface) do + isrequired + desc "The interface to use for the route" + end +end \ No newline at end of file diff --git a/spec/fixtures/provider/network_route/routes_spec/simple_routes b/spec/fixtures/provider/network_route/routes_spec/simple_routes new file mode 100644 index 00000000..635aa86b --- /dev/null +++ b/spec/fixtures/provider/network_route/routes_spec/simple_routes @@ -0,0 +1,3 @@ +172.28.45.0 255.255.255.0 172.18.6.2 vlan200 +172.17.67.0 255.255.255.0 172.18.6.2 vlan200 +10.10.10.0 255.255.255.0 172.18.6.2 vlan200 \ No newline at end of file diff --git a/spec/unit/provider/network_route/routes_spec.rb b/spec/unit/provider/network_route/routes_spec.rb new file mode 100644 index 00000000..08f05e07 --- /dev/null +++ b/spec/unit/provider/network_route/routes_spec.rb @@ -0,0 +1,80 @@ +#!/usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe Puppet::Type.type(:network_route).provider(:routes) do + def fixture_data(file) + basedir = File.join(PROJECT_ROOT, 'spec', 'fixtures', 'provider', 'network_route', 'routes_spec') + File.read(File.join(basedir, file)) + end + + describe "when parsing" do + it "should parse out iface lines" do + fixture = fixture_data('simple_routes') + data = described_class.parse_file('', fixture) + + data.find { |h| h[:name] == '172.17.67.0/24' }.should == { + :name => '172.17.67.0/24', + :network => '172.17.67.0', + :netmask => '255.255.255.0', + :gateway => '172.18.6.2', + :interface => 'vlan200', + } + end + + describe "when reading an invalid routes file" do + it "with missing options should fail" do + expect do + described_class.parse_file('', "192.168.1.1 255.255.255.0 172.16.0.1\n") + end.to raise_error + end + end + end + + describe "when formatting" do + let(:route1_provider) do + stub('route1_provider', + :name => '172.17.67.0', + :network => '172.17.67.0', + :netmask => '255.255.255.0', + :gateway => '172.18.6.2', + :interface => 'vlan200' + ) + end + + let(:route2_provider) do + stub('lo_provider', + :name => '172.28.45.0', + :network => '172.28.45.0', + :netmask => '255.255.255.0', + :gateway => '172.18.6.2', + :interface => 'eth0' + ) + end + + let(:content) { described_class.format_file('', [route1_provider, route2_provider]) } + + describe "writing the route line" do + it "should write all 4 fields" do + content.scan(/^172.17.67.0 .*$/).length.should == 1 + content.scan(/^172.17.67.0 .*$/).first.split(' ').length.should == 4 + end + + it "should have the correct fields appended" do + content.scan(/^172.17.67.0 .*$/).first.should be_include("172.17.67.0 255.255.255.0 172.18.6.2 vlan200") + end + + it "should fail if the netmask property is not defined" do + route2_provider.unstub(:netmask) + route2_provider.stubs(:netmask).returns nil + expect { content }.to raise_exception + end + + it "should fail if the gateway property is not defined" do + route2_provider.unstub(:gateway) + route2_provider.stubs(:gateway).returns nil + expect { content }.to raise_exception + end + end + end +end \ No newline at end of file diff --git a/spec/unit/type/network_route_spec.rb b/spec/unit/type/network_route_spec.rb new file mode 100644 index 00000000..306354d2 --- /dev/null +++ b/spec/unit/type/network_route_spec.rb @@ -0,0 +1,67 @@ +#!/usr/bin/env ruby -S rspec + +require 'spec_helper' + +describe Puppet::Type.type(:network_route) do + before do + provider_class = stub 'provider class', :name => "fake", :suitable? => true, :supports_parameter? => true + provider_class.stubs(:new) + + described_class.stubs(:defaultprovider).returns provider_class + described_class.stubs(:provider).returns provider_class + end + + describe "when validating the attribute" do + + describe :name do + it { described_class.attrtype(:name).should == :param } + end + + [:ensure, :network, :netmask, :gateway, :interface].each do |property| + describe property do + it { described_class.attrtype(property).should == :property } + end + end + + it "use the name parameter as the namevar" do + described_class.key_attributes.should == [:name] + end + + describe "ensure" do + it "should be an ensurable value" do + described_class.propertybyname(:ensure).ancestors.should be_include(Puppet::Property::Ensure) + end + end + end + + describe "when validating the attribute value" do + describe "network" do + it "should validate the network as an IP address" do + expect do + described_class.new(:name => '192.168.1.0/24', :network => 'not an ip address', :netmask => '255.255.255.0', :gateway => '23.23.23.42', :interface => 'eth0') + end.to raise_error + end + end + + describe "netmask" do + it "should fail if an invalid netmask is used" do + expect do + described_class.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') + end.to raise_error + end + + it "should convert netmasks of the CIDR form" do + r = described_class.new(:name => '192.168.1.0/24', :network => '192.168.1.0', :netmask => '24', :gateway => '23.23.23.42', :interface => 'eth0') + r[:netmask].should == '255.255.255.0' + end + end + + describe "gateway" do + it "should validate as an IP address" do + expect do + described_class.new(:name => '192.168.1.0/24', :network => '192.168.1.0', :netmask => '255.255.255.0', :gateway => 'not an ip address', :interface => 'eth0') + end.to raise_error + end + end + end +end