Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for non-volatile network routes on Debian #23

Merged
merged 1 commit into from
Jan 23, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.markdown
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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
Expand All @@ -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

Expand Down
111 changes: 111 additions & 0 deletions lib/puppet/provider/network_route/routes.rb
Original file line number Diff line number Diff line change
@@ -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
58 changes: 58 additions & 0 deletions lib/puppet/type/network_route.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
80 changes: 80 additions & 0 deletions spec/unit/provider/network_route/routes_spec.rb
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions spec/unit/type/network_route_spec.rb
Original file line number Diff line number Diff line change
@@ -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