diff --git a/examples/base_provision.pp b/examples/base_provision.pp new file mode 100644 index 000000000..7e53cbc05 --- /dev/null +++ b/examples/base_provision.pp @@ -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', +} diff --git a/lib/puppet/provider/quantum.rb b/lib/puppet/provider/quantum.rb new file mode 100644 index 000000000..b9380b208 --- /dev/null +++ b/lib/puppet/provider/quantum.rb @@ -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 diff --git a/lib/puppet/provider/quantum_network/quantum.rb b/lib/puppet/provider/quantum_network/quantum.rb new file mode 100644 index 000000000..5f35c9545 --- /dev/null +++ b/lib/puppet/provider/quantum_network/quantum.rb @@ -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 diff --git a/lib/puppet/type/quantum_network.rb b/lib/puppet/type/quantum_network.rb new file mode 100644 index 000000000..10c373346 --- /dev/null +++ b/lib/puppet/type/quantum_network.rb @@ -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 diff --git a/spec/unit/provider/quantum_network/quantum_spec.rb b/spec/unit/provider/quantum_network/quantum_spec.rb new file mode 100644 index 000000000..d3cd8c012 --- /dev/null +++ b/spec/unit/provider/quantum_network/quantum_spec.rb @@ -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 diff --git a/spec/unit/provider/quantum_spec.rb b/spec/unit/provider/quantum_spec.rb new file mode 100644 index 000000000..a5e907297 --- /dev/null +++ b/spec/unit/provider/quantum_spec.rb @@ -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