Add quantum_network custom type.
Change-Id: I7b7c28662733e9e733bdbbc780d6b8736223e2ea
This commit is contained in:
parent
8ead6e3b4c
commit
ed84fbb211
|
@ -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',
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue