Add quantum_network custom type.

Change-Id: I7b7c28662733e9e733bdbbc780d6b8736223e2ea
This commit is contained in:
Mathieu Gagné 2013-05-08 18:33:21 -04:00 committed by Maru Newby
parent 8ead6e3b4c
commit ed84fbb211
6 changed files with 638 additions and 0 deletions

View File

@ -0,0 +1,24 @@
#
# This manifest is intended to demonstrate how to provision the
# resources necessary to boot a vm with network connectivity provided
# by quantum.
#
keystone_tenant { 'admin':
ensure => present,
}
quantum_network { 'public':
ensure => present,
router_external => 'True',
tenant_name => 'admin',
}
keystone_tenant { 'demo':
ensure => present,
}
quantum_network { 'private':
ensure => present,
tenant_name => 'demo',
}

View File

@ -0,0 +1,143 @@
require 'puppet/util/inifile'
class Puppet::Provider::Quantum < Puppet::Provider
def self.conf_filename
'/etc/quantum/quantum.conf'
end
def self.withenv(hash, &block)
saved = ENV.to_hash
hash.each do |name, val|
ENV[name.to_s] = val
end
yield
ensure
ENV.clear
saved.each do |name, val|
ENV[name] = val
end
end
def self.quantum_credentials
@quantum_credentials ||= get_quantum_credentials
end
def self.get_quantum_credentials
auth_keys = ['auth_host', 'auth_port', 'auth_protocol',
'admin_tenant_name', 'admin_user', 'admin_password']
conf = quantum_conf
if conf and conf['keystone_authtoken'] and
auth_keys.all?{|k| !conf['keystone_authtoken'][k].nil?}
return Hash[ auth_keys.map \
{ |k| [k, conf['keystone_authtoken'][k].strip] } ]
else
raise(Puppet::Error, "File: #{conf_filename} does not contain all \
required sections. Quantum types will not work if quantum is not \
correctly configured.")
end
end
def quantum_credentials
self.class.quantum_credentials
end
def self.auth_endpoint
@auth_endpoint ||= get_auth_endpoint
end
def self.get_auth_endpoint
q = quantum_credentials
"#{q['auth_protocol']}://#{q['auth_host']}:#{q['auth_port']}/v2.0/"
end
def self.quantum_conf
return @quantum_conf if @quantum_conf
@quantum_conf = Puppet::Util::IniConfig::File.new
@quantum_conf.read(conf_filename)
@quantum_conf
end
def self.auth_quantum(*args)
q = quantum_credentials
authenv = {
:OS_AUTH_URL => self.auth_endpoint,
:OS_USERNAME => q['admin_user'],
:OS_TENANT_NAME => q['admin_tenant_name'],
:OS_PASSWORD => q['admin_password']
}
begin
withenv authenv do
quantum(args)
end
rescue Exception => e
if (e.message =~ /\[Errno 111\] Connection refused/) or
(e.message =~ /\(HTTP 400\)/)
sleep 10
withenv authenv do
quantum(args)
end
else
raise(e)
end
end
end
def auth_quantum(*args)
self.class.auth_quantum(args)
end
def self.reset
@quantum_conf = nil
@quantum_credentials = nil
end
def self.list_quantum_resources(type)
ids = []
list = auth_quantum("#{type}-list", '--format=csv',
'--column=id', '--quote=none')
(list.split("\n")[1..-1] || []).compact.collect do |line|
ids << line.strip
end
return ids
end
def self.get_quantum_resource_attrs(type, id)
attrs = {}
net = auth_quantum("#{type}-show", '--format=shell', id)
last_key = nil
(net.split("\n") || []).compact.collect do |line|
if line.include? '='
k, v = line.split('=', 2)
attrs[k] = v.gsub(/\A"|"\Z/, '')
last_key = k
else
# Handle the case of a list of values
v = line.gsub(/\A"|"\Z/, '')
attrs[last_key] = [attrs[last_key], v]
end
end
return attrs
end
def self.list_quantum_extensions
exts = []
begin
list = auth_quantum('ext-list', '--format=csv',
'--column=alias', '--quote=none')
rescue => e
if (e.message =~ /Quantum types will not work/)
# Silently return no features if configuration is not
# available so that feature definition doesn't break
# autoload.
return exts
end
raise
end
(list.split("\n")[1..-1] || []).compact.collect do |line|
exts << line.strip
end
return exts
end
end

View File

@ -0,0 +1,165 @@
require File.join(File.dirname(__FILE__), '..','..','..',
'puppet/provider/quantum')
Puppet::Type.type(:quantum_network).provide(
:quantum,
:parent => Puppet::Provider::Quantum
) do
desc <<-EOT
Quantum provider to manage quantum_network type.
Assumes that the quantum service is configured on the same host.
EOT
commands :quantum => 'quantum'
mk_resource_methods
def self.has_provider_extension?
list_quantum_extensions.include?('provider')
end
def has_provider_extension?
self.class.has_provider_extension?
end
has_feature :provider_extension if has_provider_extension?
def self.has_router_extension?
list_quantum_extensions.include?('router')
end
def has_router_extension?
self.class.has_router_extension?
end
has_feature :router_extension if has_router_extension?
def self.quantum_type
'net'
end
def self.instances
list_quantum_resources(quantum_type).collect do |id|
attrs = get_quantum_resource_attrs(quantum_type, id)
new(
:ensure => :present,
:name => attrs['name'],
:id => attrs['id'],
:admin_state_up => attrs['admin_state_up'],
:provider_network_type => attrs['provider:network_type'],
:provider_physical_network => attrs['provider:physical_network'],
:provider_segmentation_id => attrs['provider:segmentation_id'],
:router_external => attrs['router:external'],
:shared => attrs['shared'],
:tenant_id => attrs['tenant_id']
)
end
end
def self.prefetch(resources)
networks = instances
resources.keys.each do |name|
if provider = networks.find{ |net| net.name == name }
resources[name].provider = provider
end
end
end
def exists?
@property_hash[:ensure] == :present
end
def create
network_opts = Array.new
if @resource[:shared]
network_opts << '--shared'
end
if @resource[:tenant_name]
network_opts << "--tenant_id=#{get_tenant_id}"
elsif @resource[:tenant_id]
network_opts << "--tenant_id=#{@resource[:tenant_id]}"
end
if @resource[:provider_network_type]
network_opts << \
"--provider:network_type=#{@resource[:provider_network_type]}"
end
if @resource[:provider_physical_network]
network_opts << \
"--provider:physical_network=#{@resource[:provider_physical_network]}"
end
if @resource[:provider_segmentation_id]
network_opts << \
"--provider:segmentation_id=#{@resource[:provider_segmentation_id]}"
end
if @resource[:router_external]
network_opts << "--router:external=#{@resource[:router_external]}"
end
results = auth_quantum('net-create', '--format=shell',
network_opts, resource[:name])
if results =~ /Created a new network:/
@network = Hash.new
results.split("\n").compact do |line|
@network[line.split('=').first] = \
line.split('=', 2)[1].gsub(/\A"|"\Z/, '')
end
@property_hash = {
:ensure => :present,
:name => resource[:name],
:id => @network[:id],
:admin_state_up => @network[:admin_state_up],
:provider_network_type => @network[:'provider:network_type'],
:provider_physical_network => @network[:'provider:physical_network'],
:provider_segmentation_id => @network[:'provider:segmentation_id'],
:router_external => @network[:'router:external'],
:shared => @network[:shared],
:tenant_id => @network[:tenant_id],
}
else
fail("did not get expected message on network creation, got #{results}")
end
end
def get_tenant_id
@tenant_id ||= model.catalog.resource( \
"Keystone_tenant[#{resource[:tenant_name]}]").provider.id
end
def destroy
auth_quantum('net-delete', name)
@property_hash[:ensure] = :absent
end
def admin_state_up=(value)
auth_quantum('net-update', "--admin_state_up=#{value}", name)
end
def shared=(value)
auth_quantum('net-update', "--shared=#{value}", name)
end
def router_external=(value)
auth_quantum('net-update', "--router:external=#{value}", name)
end
[
:provider_network_type,
:provider_physical_network,
:provider_segmentation_id,
:tenant_id,
].each do |attr|
define_method(attr.to_s + "=") do |value|
fail("Property #{attr.to_s} does not support being updated")
end
end
end

View File

@ -0,0 +1,111 @@
Puppet::Type.newtype(:quantum_network) do
ensurable
feature :provider_extension,
"The provider extension supports provider networks."
feature :router_extension,
"The router extension supports L3 forwarding and NAT."
newparam(:name, :namevar => true) do
desc 'Symbolic name for the network'
newvalues(/.*/)
end
newproperty(:id) do
desc 'The unique id of the network'
validate do |v|
raise(Puppet::Error, 'This is a read only property')
end
end
newproperty(:admin_state_up) do
desc 'The administrative status of the network'
newvalues(/(t|T)rue/, /(f|F)alse/)
munge do |v|
v.to_s.capitalize
end
end
newproperty(:shared) do
desc 'Whether this network should be shared across all tenants or not'
newvalues(/(t|T)rue/, /(f|F)alse/)
munge do |v|
v.to_s.capitalize
end
end
newparam(:tenant_name) do
desc 'The name of the tenant which will own the network.'
end
newproperty(:tenant_id) do
desc 'A uuid identifying the tenant which will own the network.'
end
newproperty(:provider_network_type,
:required_features => :provider_extension) do
desc 'The physical mechanism by which the virtual network is realized.'
newvalues(:flat, :vlan, :local, :gre)
end
newproperty(:provider_physical_network,
:required_features => :provider_extension) do
desc <<-EOT
The name of the physical network over which the virtual network
is realized for flat and VLAN networks.
EOT
newvalues(/\S+/)
end
newproperty(:provider_segmentation_id,
:required_features => :provider_extension) do
desc 'Identifies an isolated segment on the physical network.'
munge do |v|
Integer(v)
end
end
newproperty(:router_external, :required_features => :router_extension) do
desc 'Whether this router will route traffic to an external network'
newvalues(/(t|T)rue/, /(f|F)alse/)
munge do |v|
v.to_s.capitalize
end
end
# Require the quantum-server service to be running
autorequire(:service) do
['quantum-server']
end
autorequire(:keystone_tenant) do
[self[:tenant_name]] if self[:tenant_name]
end
validate do
if self[:ensure] != :present
return
end
if (self[:provider_network_type] ||
self[:provider_physical_network] ||
self[:provider_segmentation_id])
if (self[:provider_network_type].nil? ||
self[:provider_physical_network].nil? ||
self[:provider_segmentation_id].nil?)
raise(Puppet::Error, <<-EOT
All provider properties are required when using provider extension.
EOT
)
end
end
if self[:tenant_id] && self[:tenant_name]
raise(Puppet::Error, <<-EOT
Please provide a value for only one of tenant_name and tenant_id.
EOT
)
end
end
end

View File

@ -0,0 +1,64 @@
require 'puppet'
require 'spec_helper'
require 'puppet/provider/quantum_network/quantum'
provider_class = Puppet::Type.type(:quantum_network).provider(:quantum)
describe provider_class do
let :net_name do
'net1'
end
let :net_attrs do
{
:name => net_name,
:ensure => 'present',
:admin_state_up => 'True',
:router_external => 'False',
:shared => 'False',
:tenant_id => '',
}
end
describe 'when updating a network' do
let :resource do
Puppet::Type::Quantum_network.new(net_attrs)
end
let :provider do
provider_class.new(resource)
end
it 'should call net-update to change admin_state_up' do
provider.expects(:auth_quantum).with('net-update',
'--admin_state_up=False',
net_name)
provider.admin_state_up=('False')
end
it 'should call net-update to change shared' do
provider.expects(:auth_quantum).with('net-update',
'--shared=True',
net_name)
provider.shared=('True')
end
it 'should call net-update to change router_external' do
provider.expects(:auth_quantum).with('net-update',
'--router:external=True',
net_name)
provider.router_external=('True')
end
[:provider_network_type, :provider_physical_network, :provider_segmentation_id].each do |attr|
it "should fail when #{attr.to_s} is update " do
expect do
provider.send("#{attr}=", 'foo')
end.to raise_error(Puppet::Error, /does not support being updated/)
end
end
end
end

View File

@ -0,0 +1,131 @@
require 'puppet'
require 'spec_helper'
require 'puppet/provider/quantum'
require 'tempfile'
describe Puppet::Provider::Quantum do
def klass
described_class
end
let :credential_hash do
{
'auth_host' => '192.168.56.210',
'auth_port' => '35357',
'auth_protocol' => 'https',
'admin_tenant_name' => 'admin_tenant',
'admin_user' => 'admin',
'admin_password' => 'password',
}
end
let :auth_endpoint do
'https://192.168.56.210:35357/v2.0/'
end
let :credential_error do
/Quantum types will not work/
end
after :each do
klass.reset
end
describe 'when determining credentials' do
it 'should fail if config is empty' do
conf = {}
klass.expects(:quantum_conf).returns(conf)
expect do
klass.quantum_credentials
end.to raise_error(Puppet::Error, credential_error)
end
it 'should fail if config does not have keystone_authtoken section.' do
conf = {'foo' => 'bar'}
klass.expects(:quantum_conf).returns(conf)
expect do
klass.quantum_credentials
end.to raise_error(Puppet::Error, credential_error)
end
it 'should fail if config does not contain all auth params' do
conf = {'keystone_authtoken' => {'invalid_value' => 'foo'}}
klass.expects(:quantum_conf).returns(conf)
expect do
klass.quantum_credentials
end.to raise_error(Puppet::Error, credential_error)
end
it 'should use specified host/port/protocol in the auth endpoint' do
conf = {'keystone_authtoken' => credential_hash}
klass.expects(:quantum_conf).returns(conf)
klass.get_auth_endpoint.should == auth_endpoint
end
end
describe 'when invoking the quantum cli' do
it 'should set auth credentials in the environment' do
authenv = {
:OS_AUTH_URL => auth_endpoint,
:OS_USERNAME => credential_hash['admin_user'],
:OS_TENANT_NAME => credential_hash['admin_tenant_name'],
:OS_PASSWORD => credential_hash['admin_password'],
}
klass.expects(:get_quantum_credentials).with().returns(credential_hash)
klass.expects(:withenv).with(authenv)
klass.auth_quantum('test_retries')
end
['[Errno 111] Connection refused',
'(HTTP 400)'].reverse.each do |valid_message|
it "should retry when quantum cli returns with error #{valid_message}" do
klass.expects(:get_quantum_credentials).with().returns({})
klass.expects(:sleep).with(10).returns(nil)
klass.expects(:quantum).twice.with(['test_retries']).raises(
Exception, valid_message).then.returns('')
klass.auth_quantum('test_retries')
end
end
end
describe 'when listing quantum resources' do
it 'should exclude the column header' do
output = <<-EOT
id
net1
net2
EOT
klass.expects(:auth_quantum).returns(output)
result = klass.list_quantum_resources('foo')
result.should eql(['net1', 'net2'])
end
end
describe 'when retrieving attributes for quantum resources' do
it 'should parse single-valued attributes into a key-value pair' do
klass.expects(:auth_quantum).returns('admin_state_up="True"')
result = klass.get_quantum_resource_attrs('foo', 'id')
result.should eql({"admin_state_up" => 'True'})
end
it 'should parse multi-valued attributes into a key-list pair' do
output = <<-EOT
subnets="subnet1
subnet2"
EOT
klass.expects(:auth_quantum).returns(output)
result = klass.get_quantum_resource_attrs('foo', 'id')
result.should eql({"subnets" => ['subnet1', 'subnet2']})
end
end
end