require 'csv' require 'puppet' class Puppet::Error::OpenstackAuthInputError < Puppet::Error end class Puppet::Error::OpenstackUnauthorizedError < Puppet::Error end 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] 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 end sleep(2) else raise e end end 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) [ '--os-token', credentials['token'], '--os-url', credentials['url'] ] 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