diff --git a/lib/puppet/provider/openstack.rb b/lib/puppet/provider/openstack.rb index 3dccba1a..155e5b91 100644 --- a/lib/puppet/provider/openstack.rb +++ b/lib/puppet/provider/openstack.rb @@ -12,185 +12,73 @@ class Puppet::Provider::Openstack < Puppet::Provider initvars # so commands will work commands :openstack => 'openstack' - def request(service, action, object, credentials, *properties) - if password_credentials_set?(credentials) - auth_args = password_auth_args(credentials) - elsif openrc_set?(credentials) - credentials = get_credentials_from_openrc(credentials['openrc']) - auth_args = password_auth_args(credentials) - elsif service_credentials_set?(credentials) - auth_args = token_auth_args(credentials) - elsif env_vars_set? - # noop; auth needs no extra arguments - auth_args = nil - else # All authentication efforts failed - raise(Puppet::Error::OpenstackAuthInputError, 'No credentials provided.') - end - args = [object, properties, auth_args].flatten.compact - authenticate_request(service, action, args) - end - - def self.request(service, action, object, *properties) - if env_vars_set? - # noop; auth needs no extra arguments - auth_args = nil - else # All authentication efforts failed - raise(Puppet::Error::OpenstackAuthInputError, 'No credentials provided.') - end - args = [object, properties, auth_args].flatten.compact - authenticate_request(service, action, args) - end - # Returns an array of hashes, where the keys are the downcased CSV headers # with underscores instead of spaces - def self.authenticate_request(service, action, *args) - rv = nil - timeout = 10 - end_time = Time.now.to_i + timeout - loop do - begin - if(action == 'list') - response = openstack(service, action, '--quiet', '--format', 'csv', args) - response = parse_csv(response) - keys = response.delete_at(0) # ID,Name,Description,Enabled - rv = response.collect do |line| - hash = {} - keys.each_index do |index| - key = keys[index].downcase.gsub(/ /, '_').to_sym - hash[key] = line[index] + def self.request(service, action, properties, credentials=nil) + env = credentials ? credentials.to_env : {} + Puppet::Util.withenv(env) do + rv = nil + timeout = 10 + end_time = Time.now.to_i + timeout + loop do + begin + if(action == 'list') + response = openstack(service, action, '--quiet', '--format', 'csv', properties) + response = parse_csv(response) + keys = response.delete_at(0) # ID,Name,Description,Enabled + rv = response.collect do |line| + hash = {} + keys.each_index do |index| + key = keys[index].downcase.gsub(/ /, '_').to_sym + hash[key] = line[index] + end + hash + end + elsif(action == 'show' || action == 'create') + rv = {} + # shell output is name="value"\nid="value2"\ndescription="value3" etc. + openstack(service, action, '--format', 'shell', properties).split("\n").each do |line| + # key is everything before the first "=" + key, val = line.split("=", 2) + next unless val # Ignore warnings + # value is everything after the first "=", with leading and trailing double quotes stripped + val = val.gsub(/\A"|"\Z/, '') + rv[key.downcase.to_sym] = val end - hash - end - elsif(action == 'show' || action == 'create') - rv = {} - # shell output is name="value"\nid="value2"\ndescription="value3" etc. - openstack(service, action, '--format', 'shell', args).split("\n").each do |line| - # key is everything before the first "=" - key, val = line.split("=", 2) - next unless val # Ignore warnings - # value is everything after the first "=", with leading and trailing double quotes stripped - val = val.gsub(/\A"|"\Z/, '') - rv[key.downcase.to_sym] = val - end - else - rv = openstack(service, action, args) - end - break - rescue Puppet::ExecutionFailure => e - if e.message =~ /HTTP 401/ - raise(Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate.') - elsif e.message =~ /Unable to establish connection/ - current_time = Time.now.to_i - if current_time > end_time - break else - wait = end_time - current_time - Puppet::debug("Non-fatal error: \"#{e.message}\"; retrying for #{wait} more seconds.") - if wait > timeout - 2 # Only notice the first time - notice("#{service} service is unavailable. Will retry for up to #{wait} seconds.") - end + rv = openstack(service, action, properties) + end + break + rescue Puppet::ExecutionFailure => e + if e.message =~ /HTTP 401/ + raise(Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate.') + elsif e.message =~ /Unable to establish connection/ + current_time = Time.now.to_i + if current_time > end_time + break + else + wait = end_time - current_time + Puppet::debug("Non-fatal error: \"#{e.message}\"; retrying for #{wait} more seconds.") + if wait > timeout - 2 # Only notice the first time + notice("#{service} service is unavailable. Will retry for up to #{wait} seconds.") + end + end + sleep(2) + else + raise e end - sleep(2) - else - raise e end end + return rv end - return rv - end - - def authenticate_request(service, action, *args) - self.class.authenticate_request(service, action, *args) end private - def password_credentials_set?(auth_params) - auth_params && auth_params['username'] && auth_params['password'] && auth_params['project_name'] && auth_params['auth_url'] - end - - def openrc_set?(auth_params) - auth_params && auth_params['openrc'] - end - - def service_credentials_set?(auth_params) - auth_params && auth_params['token'] && auth_params['url'] - end - - def self.env_vars_set? - ENV['OS_USERNAME'] && ENV['OS_PASSWORD'] && ENV['OS_PROJECT_NAME'] && ENV['OS_AUTH_URL'] - end - - def env_vars_set? - self.class.env_vars_set? - end - - def self.password_auth_args(credentials) - creds = [ '--os-username', credentials['username'], - '--os-password', credentials['password'], - '--os-project-name', credentials['project_name'], - '--os-auth-url', credentials['auth_url'] ] - - if credentials.include?('project_domain_name') - creds << '--os-project-domain-name' - creds << credentials['project_domain_name'] - end - - if credentials.include?('user_domain_name') - creds << '--os-user-domain-name' - creds << credentials['user_domain_name'] - end - - creds - end - - def password_auth_args(credentials) - self.class.password_auth_args(credentials) - end - - def self.token_auth_args(credentials) - args = [ '--os-token', credentials['token'], - '--os-url', credentials['url'] ] - # Add the api version only if requested by the caller - if credentials['version'] - args << [ '--os-identity-api-version', credentials['version'] ] - end - args - end - - def token_auth_args(credentials) - self.class.token_auth_args(credentials) - end - - def get_credentials_from_openrc(file) - creds = {} - File.open(file).readlines.delete_if{|l| l=~ /^#/}.each do |line| - key, value = line.split('=') - key = key.split(' ').last.downcase.sub(/^os_/, '') - value = value.chomp.gsub(/'/, '') - creds[key] = value - end - return creds - end - - def self.get_credentials_from_env - env = ENV.to_hash.dup.delete_if { |key, _| ! (key =~ /^OS_/) } - credentials = {} - env.each do |name, value| - credentials[name.downcase.sub(/^os_/, '')] = value - end - credentials - end - - def get_credentials_from_env - self.class.get_credentials_from_env - end - def self.parse_csv(text) # Ignore warnings - assume legitimate output starts with a double quoted # string. Errors will be caught and raised prior to this text = text.split("\n").drop_while { |line| line !~ /^\".*\"/ }.join("\n") return CSV.parse(text + "\n") end - end diff --git a/lib/puppet/provider/openstack/auth.rb b/lib/puppet/provider/openstack/auth.rb new file mode 100644 index 00000000..93a054ee --- /dev/null +++ b/lib/puppet/provider/openstack/auth.rb @@ -0,0 +1,49 @@ +require 'puppet/provider/openstack/credentials' + +module Puppet::Provider::Openstack::Auth + + RCFILENAME = "#{ENV['HOME']}/openrc" + + def get_os_vars_from_env + env = {} + ENV.each { |k,v| env.merge!(k => v) if k =~ /^OS_/ } + return env + end + + def get_os_vars_from_rcfile(filename) + env = {} + if File.exists?(filename) + File.open(filename).readlines.delete_if{|l| l=~ /^#|^$/ }.each do |line| + key, value = line.split('=') + key = key.split(' ').last + value = value.chomp.gsub(/'/, '') + env.merge!(key => value) if key =~ /OS_/ + end + end + return env + end + + def rc_filename + RCFILENAME + end + + def request(service, action, properties=nil) + properties ||= [] + set_credentials(@credentials, get_os_vars_from_env) + unless @credentials.set? + @credentials.unset + set_credentials(@credentials, get_os_vars_from_rcfile(rc_filename)) + end + unless @credentials.set? + raise(Puppet::Error::OpenstackAuthInputError, 'Insufficient credentials to authenticate') + end + super(service, action, properties, @credentials) + end + + def set_credentials(creds, env) + env.each do |key, val| + var = key.sub(/^OS_/,'').downcase + creds.set(var, val) + end + end +end diff --git a/lib/puppet/provider/openstack/credentials.rb b/lib/puppet/provider/openstack/credentials.rb new file mode 100644 index 00000000..3b128b60 --- /dev/null +++ b/lib/puppet/provider/openstack/credentials.rb @@ -0,0 +1,56 @@ +require 'puppet' +require 'puppet/provider/openstack' + +class Puppet::Provider::Openstack::Credentials + + KEYS = [ + :auth_url, :password, :project_name, :username, + :token, :url, + :identity_api_version + ] + + KEYS.each { |var| attr_accessor var } + + def self.defined?(name) + KEYS.include?(name.to_sym) + end + + def set(key, val) + if self.class.defined?(key.to_sym) + self.instance_variable_set("@#{key}".to_sym, val) + end + end + + def set? + return true if user_password_set? || service_token_set? + end + + def service_token_set? + return true if @token && @url + end + + def to_env + env = {} + self.instance_variables.each do |var| + name = var.to_s.sub(/^@/,'OS_').upcase + env.merge!(name => self.instance_variable_get(var)) + end + env + end + + def user_password_set? + return true if @username && @password && @project_name && @auth_url + end + + def unset + list = KEYS.delete_if { |key, val| key == :identity_api_version } + list.each { |key, val| self.set(key, '') if self.class.defined?("@#{key}".to_sym) } + end + + def version + self.class.to_s.sub(/.*V/,'').sub('_','.') + end +end + +class Puppet::Provider::Openstack::CredentialsV2_0 < Puppet::Provider::Openstack::Credentials +end diff --git a/lib/puppet/util/openstack.rb b/lib/puppet/util/openstack.rb deleted file mode 100644 index 916aa8e1..00000000 --- a/lib/puppet/util/openstack.rb +++ /dev/null @@ -1,66 +0,0 @@ -# Add the auth parameter to whatever type is given -module Puppet::Util::Openstack - def self.add_openstack_type_methods(type, comment) - - type.newparam(:auth) do - - desc < { - 'username' => 'test', - 'password' => 'changeme', - 'project_name' => 'test', - 'auth_url' => 'http://localhost:35357/v2.0' -} - -or altenatively for Keystone API V3: -auth => { - 'username' => 'test', - 'password' => 'changeme', - 'project_name' => 'test', - 'project_domain_name' => 'domain1', - 'user_domain_name' => 'domain1', - 'auth_url' => 'http://localhost:35357/v3' -} - -2. Using a path to an openrc file containing these credentials - -auth => { - 'openrc' => '/root/openrc' -} - -3. Using a service token - -For Keystone API V2: -auth => { - 'token' => 'example', - 'url' => 'http://localhost:35357/v2.0' -} - -Alternatively for Keystone API V3: -auth => { - 'token' => 'example', - 'url' => 'http://localhost:35357/v3.0' -} - -If not present, the provider will look for environment variables for -password credentials. - -#{comment} -EOT - - validate do |value| - raise(Puppet::Error, 'This property must be a hash') unless value.is_a?(Hash) - end - end - - type.autorequire(:package) do - 'python-openstackclient' - end - - end -end diff --git a/spec/unit/provider/openstack/auth_spec.rb b/spec/unit/provider/openstack/auth_spec.rb new file mode 100644 index 00000000..77a8f276 --- /dev/null +++ b/spec/unit/provider/openstack/auth_spec.rb @@ -0,0 +1,181 @@ +require 'puppet' +require 'spec_helper' +require 'puppet/provider/openstack' +require 'puppet/provider/openstack/auth' +require 'tempfile' + +class Puppet::Provider::Openstack::AuthTester < Puppet::Provider::Openstack + extend Puppet::Provider::Openstack::Auth +end + +klass = Puppet::Provider::Openstack::AuthTester + +describe Puppet::Provider::Openstack::Auth do + + let(:type) do + Puppet::Type.newtype(:test_resource) do + newparam(:name, :namevar => true) + newparam(:log_file) + end + end + + let(:resource_attrs) do + { + :name => 'stubresource' + } + end + + let(:provider) do + klass.new(type.new(resource_attrs)) + end + + before(:each) do + ENV['OS_USERNAME'] = nil + ENV['OS_PASSWORD'] = nil + ENV['OS_PROJECT_NAME'] = nil + ENV['OS_AUTH_URL'] = nil + ENV['OS_TOKEN'] = nil + ENV['OS_URL'] = nil + end + + describe '#set_credentials' do + it 'adds keys to the object' do + credentials = Puppet::Provider::Openstack::CredentialsV2_0.new + set = { 'OS_USERNAME' => 'user', 'OS_PASSWORD' => 'secret', + 'OS_PROJECT_NAME' => 'tenant', + 'OS_AUTH_URL' => 'http://127.0.0.1:5000', + 'OS_TOKEN' => 'token', + 'OS_URL' => 'http://127.0.0.1:35357', + 'OS_IDENTITY_API_VERSION' => '2.0' + } + klass.set_credentials(credentials, set) + expect(credentials.to_env).to eq("OS_AUTH_URL" => "http://127.0.0.1:5000", + "OS_IDENTITY_API_VERSION" => '2.0', + "OS_PASSWORD" => "secret", + "OS_PROJECT_NAME" => "tenant", + "OS_TOKEN" => "token", + "OS_URL" => "http://127.0.0.1:35357", + "OS_USERNAME" => "user") + end + end + + describe '#rc_filename' do + it 'returns RCFILENAME' do + expect(klass.rc_filename).to eq("#{ENV['HOME']}/openrc") + end + end + + describe '#get_os_from_env' do + context 'with Openstack environment variables set' do + it 'provides a hash' do + ENV['OS_AUTH_URL'] = 'http://127.0.0.1:5000' + ENV['OS_PASSWORD'] = 'abc123' + ENV['OS_PROJECT_NAME'] = 'test' + ENV['OS_USERNAME'] = 'test' + response = klass.get_os_vars_from_env + expect(response).to eq({"OS_AUTH_URL" => "http://127.0.0.1:5000","OS_PASSWORD" => "abc123","OS_PROJECT_NAME" => "test","OS_USERNAME" => "test"}) + end + end + end + + describe '#get_os_vars_from_rcfile' do + context 'with a valid RC file' do + it 'provides a hash' do + mock = "export OS_USERNAME='test'\nexport OS_PASSWORD='abc123'\nexport OS_PROJECT_NAME='test'\nexport OS_AUTH_URL='http://127.0.0.1:5000'" + filename = 'file' + File.expects(:exists?).with('file').returns(true) + File.expects(:open).with('file').returns(StringIO.new(mock)) + + response = klass.get_os_vars_from_rcfile(filename) + expect(response).to eq({"OS_AUTH_URL" => "http://127.0.0.1:5000","OS_PASSWORD" => "abc123","OS_PROJECT_NAME" => "test","OS_USERNAME" => "test"}) + end + end + + context 'with an empty file' do + it 'provides an empty hash' do + filename = 'file' + File.expects(:exists?).with(filename).returns(true) + File.expects(:open).with(filename).returns(StringIO.new("")) + + response = klass.get_os_vars_from_rcfile(filename) + expect(response).to eq({}) + end + end + end + + before(:each) do + class Puppet::Provider::Openstack::AuthTester + @credentials = Puppet::Provider::Openstack::CredentialsV2_0.new + end + end + + describe '#request' do + context 'with no valid credentials' do + it 'fails to authenticate' do + expect { klass.request('project', 'list', ['--long']) }.to raise_error(Puppet::Error::OpenstackAuthInputError, "Insufficient credentials to authenticate") + end + end + + context 'with user credentials in env' do + it 'is successful' do + klass.expects(:get_os_vars_from_env) + .returns({ 'OS_USERNAME' => 'test', + 'OS_PASSWORD' => 'abc123', + 'OS_PROJECT_NAME' => 'test', + 'OS_AUTH_URL' => 'http://127.0.0.1:5000' }) + klass.expects(:openstack) + .with('project', 'list', '--quiet', '--format', 'csv', ['--long']) + .returns('"ID","Name","Description","Enabled" +"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True +') + response = klass.request('project', 'list', ['--long']) + expect(response.first[:description]).to eq("Test tenant") + end + end + + context 'with service token credentials in env' do + it 'is successful' do + klass.expects(:get_os_vars_from_env) + .returns({ 'OS_TOKEN' => 'test', + 'OS_URL' => 'http://127.0.0.1:5000' }) + klass.expects(:openstack) + .with('project', 'list', '--quiet', '--format', 'csv', ['--long']) + .returns('"ID","Name","Description","Enabled" +"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True +') + response = klass.request('project', 'list', ['--long']) + expect(response.first[:description]).to eq("Test tenant") + end + end + + context 'with a RC file containing user credentials' do + it 'is successful' do + mock = "export OS_USERNAME='test'\nexport OS_PASSWORD='abc123'\nexport OS_PROJECT_NAME='test'\nexport OS_AUTH_URL='http://127.0.0.1:5000'" + File.expects(:exists?).with("#{ENV['HOME']}/openrc").returns(true) + File.expects(:open).with("#{ENV['HOME']}/openrc").returns(StringIO.new(mock)) + klass.expects(:openstack) + .with('project', 'list', '--quiet', '--format', 'csv', ['--long']) + .returns('"ID","Name","Description","Enabled" +"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True +') + response = provider.class.request('project', 'list', ['--long']) + expect(response.first[:description]).to eq("Test tenant") + end + end + + context 'with a RC file containing service token credentials' do + it 'is successful' do + mock = "export OS_TOKEN='test'\nexport OS_URL='abc123'\n" + File.expects(:exists?).with("#{ENV['HOME']}/openrc").returns(true) + File.expects(:open).with("#{ENV['HOME']}/openrc").returns(StringIO.new(mock)) + klass.expects(:openstack) + .with('project', 'list', '--quiet', '--format', 'csv', ['--long']) + .returns('"ID","Name","Description","Enabled" +"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True +') + response = klass.request('project', 'list', ['--long']) + expect(response.first[:description]).to eq("Test tenant") + end + end + end +end diff --git a/spec/unit/provider/openstack/credentials_spec.rb b/spec/unit/provider/openstack/credentials_spec.rb new file mode 100644 index 00000000..2fed6da0 --- /dev/null +++ b/spec/unit/provider/openstack/credentials_spec.rb @@ -0,0 +1,63 @@ +require 'puppet' +require 'spec_helper' +require 'puppet/provider/openstack' +require 'puppet/provider/openstack/credentials' + + +describe Puppet::Provider::Openstack::Credentials do + + let(:creds) do + creds = Puppet::Provider::Openstack::CredentialsV2_0.new + end + + describe '#service_token_set?' do + context "with service credentials" do + it 'is successful' do + creds.token = 'token' + creds.url = 'url' + expect(creds.service_token_set?).to be_truthy + end + end + end + + describe '#password_set?' do + context "with user credentials" do + it 'is successful' do + creds.auth_url = 'auth_url' + creds.password = 'password' + creds.project_name = 'project_name' + creds.username = 'username' + expect(creds.user_password_set?).to be_truthy + end + end + end + + describe '#set?' do + context "without any credential" do + it 'fails' do + expect(creds.set?).to be_falsey + end + end + end + + describe '#to_env' do + context "with an exhaustive data set" do + it 'successfully returns content' do + creds.auth_url = 'auth_url' + creds.password = 'password' + creds.project_name = 'project_name' + creds.username = 'username' + creds.token = 'token' + creds.url = 'url' + creds.identity_api_version = 'identity_api_version' + expect(creds.auth_url).to eq("auth_url") + expect(creds.password).to eq("password") + expect(creds.project_name).to eq("project_name") + expect(creds.username).to eq("username") + expect(creds.token).to eq('token') + expect(creds.url).to eq('url') + expect(creds.identity_api_version).to eq('identity_api_version') + end + end + end +end diff --git a/spec/unit/provider/openstack_spec.rb b/spec/unit/provider/openstack_spec.rb index 5ae7dafd..c9c22eed 100644 --- a/spec/unit/provider/openstack_spec.rb +++ b/spec/unit/provider/openstack_spec.rb @@ -1,12 +1,8 @@ -# Load libraries from aviator here to simulate how they live together in a real puppet run -$LOAD_PATH.push(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'modules', 'aviator', 'lib')) require 'puppet' require 'spec_helper' require 'puppet/provider/openstack' - describe Puppet::Provider::Openstack do - before(:each) do ENV['OS_USERNAME'] = nil ENV['OS_PASSWORD'] = nil @@ -17,239 +13,48 @@ describe Puppet::Provider::Openstack do let(:type) do Puppet::Type.newtype(:test_resource) do newparam(:name, :namevar => true) - newparam(:auth) newparam(:log_file) end end - shared_examples 'authenticating with environment variables using API v2' do - it 'makes a successful request' do - ENV['OS_USERNAME'] = 'test' - ENV['OS_PASSWORD'] = 'abc123' - ENV['OS_PROJECT_NAME'] = 'test' - ENV['OS_AUTH_URL'] = 'http://127.0.0.1:35357/v2.0' - if provider.class == Class - provider.stubs(:openstack) - .with('project', 'list', '--quiet', '--format', 'csv', [[ '--long' ]]) - .returns('"ID","Name","Description","Enabled" -"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True -') - else - provider.class.stubs(:openstack) - .with('project', 'list', '--quiet', '--format', 'csv', [[ '--long' ]]) - .returns('"ID","Name","Description","Enabled" -"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True -') - end - response = provider.request('project', 'list', nil, nil, '--long' ) - expect(response.first[:description]).to match /Test tenant/ - end - end - - shared_examples 'authenticating with environment variables using API v3' do - it 'makes a successful request' do - ENV['OS_USERNAME'] = 'test' - ENV['OS_PASSWORD'] = 'abc123' - ENV['OS_PROJECT_NAME'] = 'test' - ENV['OS_AUTH_URL'] = 'http://127.0.0.1:35357/v3' - if provider.class == Class - provider.stubs(:openstack) - .with('project', 'list', '--quiet', '--format', 'csv', [[ '--long' ]]) - .returns('"ID","Name","Description","Enabled" -"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True -') - else - provider.class.stubs(:openstack) - .with('project', 'list', '--quiet', '--format', 'csv', [[ '--long' ]]) - .returns('"ID","Name","Description","Enabled" -"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True -') - end - response = provider.request('project', 'list', nil, nil, '--long' ) - expect(response.first[:description]).to match /Test tenant/ - end - end - - shared_examples 'it has no credentials' do - it 'fails to authenticate' do - expect{ provider.request('project', 'list', nil, nil, '--long') }.to raise_error(Puppet::Error::OpenstackAuthInputError, /No credentials provided/) - end - end - describe '#request' do + let(:resource_attrs) do + { + :name => 'stubresource', + } + end - context 'with valid password credentials in parameters' do - let(:resource_attrs) do - { - :name => 'stubresource', - :auth => { - 'username' => 'test', - 'password' => 'abc123', - 'project_name' => 'test', - 'auth_url' => 'http://127.0.0.1:5000/v2.0', - } - } - end - let(:provider) do - Puppet::Provider::Openstack.new(type.new(resource_attrs)) - end + let(:provider) do + Puppet::Provider::Openstack.new(type.new(resource_attrs)) + end - it 'makes a successful request' do - provider.class.stubs(:openstack) - .with('project', 'list', '--quiet', '--format', 'csv', [['--long', '--os-username', 'test', '--os-password', 'abc123', '--os-project-name', 'test', '--os-auth-url', 'http://127.0.0.1:5000/v2.0']]) - .returns('"ID","Name","Description","Enabled" + it 'makes a successful request' do + provider.class.stubs(:openstack) + .with('project', 'list', '--quiet', '--format', 'csv', ['--long']) + .returns('"ID","Name","Description","Enabled" "1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True ') - response = provider.request('project', 'list', nil, resource_attrs[:auth], '--long') - expect(response.first[:description]).to match /Test tenant/ - end + response = Puppet::Provider::Openstack.request('project', 'list', ['--long']) + expect(response.first[:description]).to eq("Test tenant") end - context 'with valid openrc file in parameters' do - mock = "export OS_USERNAME='test'\nexport OS_PASSWORD='abc123'\nexport OS_PROJECT_NAME='test'\nexport OS_AUTH_URL='http://127.0.0.1:5000/v2.0'" - let(:resource_attrs) do - { - :name => 'stubresource', - :auth => { - 'openrc' => '/root/openrc' - } - } - end - let(:provider) do - Puppet::Provider::Openstack.new(type.new(resource_attrs)) - end - - it 'makes a successful request' do - File.expects(:open).with('/root/openrc').returns(StringIO.new(mock)) - provider.class.stubs(:openstack) - .with('project', 'list', '--quiet', '--format', 'csv', [['--long', '--os-username', 'test', '--os-password', 'abc123', '--os-project-name', 'test', '--os-auth-url', 'http://127.0.0.1:5000/v2.0']]) - .returns('"ID","Name","Description","Enabled" -"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True -') - response = provider.request('project', 'list', nil, resource_attrs[:auth], '--long') - expect(response.first[:description]).to match /Test tenant/ - end - end - - context 'with valid service token in parameters' do - let(:resource_attrs) do - { - :name => 'stubresource', - :auth => { - 'token' => 'secrettoken', - 'url' => 'http://127.0.0.1:5000/v3', - 'version' => '3' - } - } - end - let(:provider) do - Puppet::Provider::Openstack.new(type.new(resource_attrs)) - end - - it 'makes a successful request' do - provider.class.stubs(:openstack) - .with('project', 'list', '--quiet', '--format', 'csv', [['--long', '--os-token', 'secrettoken', '--os-url', 'http://127.0.0.1:5000/v3', '--os-identity-api-version', '3']]) - .returns('"ID","Name","Description","Enabled" -"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True -') - response = provider.request('project', 'list', nil, resource_attrs[:auth], '--long') - expect(response.first[:description]).to match /Test tenant/ - end - - it 'makes a successful show request' do - provider.class.stubs(:openstack) - .with('project', 'show', '--format', 'shell', [['test', '--os-token', 'secrettoken', '--os-url', 'http://127.0.0.1:5000/v3', '--os-identity-api-version', '3']]) - .returns('ID="1cb05cfed7c24279be884ba4f6520262" -Name="test" -Description="Test Tenant" -Enabled="True" -') - response = provider.request('project', 'show', 'test', resource_attrs[:auth]) - expect(response[:description]).to match /Test Tenant/ - expect(response[:id]).to match /1cb05cfed7c24279be884ba4f6520262/ - expect(response[:name]).to match /test/ - expect(response[:enabled]).to match /True/ - end - - end - - context 'with valid password credentials in environment variables' do - it_behaves_like 'authenticating with environment variables using API v2' do - let(:resource_attrs) do - { - :name => 'stubresource', - } - end - let(:provider) do - Puppet::Provider::Openstack.new(type.new(resource_attrs)) - end - end - end - - context 'with no valid credentials' do - it_behaves_like 'it has no credentials' do - let(:resource_attrs) do - { - :name => 'stubresource', - } - end - let(:provider) do - Puppet::Provider::Openstack.new(type.new(resource_attrs)) - end - end - end - - context 'it retries on connection errors' do - let(:resource_attrs) do - { - :name => 'stubresource', - :auth => { - 'username' => 'test', - 'password' => 'abc123', - 'project_name' => 'test', - 'auth_url' => 'http://127.0.0.1:5000/v2.0', - } - } - end - let(:provider) do - Puppet::Provider::Openstack.new(type.new(resource_attrs)) - end + context 'on connection errors' do it 'retries' do + ENV['OS_USERNAME'] = 'test' + ENV['OS_PASSWORD'] = 'abc123' + ENV['OS_PROJECT_NAME'] = 'test' + ENV['OS_AUTH_URL'] = 'http://127.0.0.1:5000' provider.class.stubs(:openstack) - .with('project', 'list', '--quiet', '--format', 'csv', [['--long', '--os-username', 'test', '--os-password', 'abc123', '--os-project-name', 'test', '--os-auth-url', 'http://127.0.0.1:5000/v2.0']]) + .with('project', 'list', '--quiet', '--format', 'csv', ['--long']) .raises(Puppet::ExecutionFailure, 'Unable to establish connection') .then .returns('') provider.class.expects(:sleep).with(2).returns(nil) - provider.request('project', 'list', nil, resource_attrs[:auth], '--long') + Puppet::Provider::Openstack.request('project', 'list', ['--long']) end end end - - describe '::request' do - - context 'with valid password credentials in environment variables' do - it_behaves_like 'authenticating with environment variables using API v2' do - let(:resource_attrs) do - { - :name => 'stubresource', - } - end - let(:provider) do - Puppet::Provider::Openstack.dup - end - end - end - - context 'with no valid credentials' do - it_behaves_like 'it has no credentials' do - let(:provider) { Puppet::Provider::Openstack.dup } - end - end - - end - describe 'parse_csv' do context 'with mixed stderr' do text = "ERROR: Testing\n\"field\",\"test\",1,2,3\n" @@ -281,5 +86,4 @@ Enabled="True" end end end - end