1 # TODO: This needs to be extracted out into openstacklib in the Kilo cycle
5 class Puppet::Error::OpenstackAuthInputError < Puppet::Error
8 class Puppet::Error::OpenstackUnauthorizedError < Puppet::Error
11 class Puppet::Provider::Openstack < Puppet::Provider
13 initvars # so commands will work
14 commands :openstack => 'openstack'
16 def request(service, action, object, credentials, *properties)
17 if password_credentials_set?(credentials)
18 auth_args = password_auth_args(credentials)
19 elsif openrc_set?(credentials)
20 credentials = get_credentials_from_openrc(credentials['openrc'])
21 auth_args = password_auth_args(credentials)
22 elsif service_credentials_set?(credentials)
23 auth_args = token_auth_args(credentials)
25 # noop; auth needs no extra arguments
27 else # All authentication efforts failed
28 raise(Puppet::Error::OpenstackAuthInputError, 'No credentials provided.')
30 args = [object, properties, auth_args].flatten.compact
31 authenticate_request(service, action, args)
34 def self.request(service, action, object, *properties)
36 # noop; auth needs no extra arguments
38 else # All authentication efforts failed
39 raise(Puppet::Error::OpenstackAuthInputError, 'No credentials provided.')
41 args = [object, properties, auth_args].flatten.compact
42 authenticate_request(service, action, args)
45 # Returns an array of hashes, where the keys are the downcased CSV headers
46 # with underscores instead of spaces
47 def self.authenticate_request(service, action, *args)
50 end_time = Time.now.to_i + timeout
54 response = openstack(service, action, '--quiet', '--format', 'csv', args)
55 response = parse_csv(response)
56 keys = response.delete_at(0) # ID,Name,Description,Enabled
57 rv = response.collect do |line|
59 keys.each_index do |index|
60 key = keys[index].downcase.gsub(/ /, '_').to_sym
61 hash[key] = line[index]
65 elsif(action == 'show' || action == 'create')
67 # shell output is name="value"\nid="value2"\ndescription="value3" etc.
68 openstack(service, action, '--format', 'shell', args).split("\n").each do |line|
69 # key is everything before the first "="
70 key, val = line.split("=", 2)
71 next unless val # Ignore warnings
72 # value is everything after the first "=", with leading and trailing double quotes stripped
73 val = val.gsub(/\A"|"\Z/, '')
74 rv[key.downcase.to_sym] = val
77 rv = openstack(service, action, args)
80 rescue Puppet::ExecutionFailure => e
81 if e.message =~ /HTTP 401/
82 raise(Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate.')
83 elsif e.message =~ /Unable to establish connection/
84 current_time = Time.now.to_i
85 if current_time > end_time
88 wait = end_time - current_time
89 Puppet::debug("Non-fatal error: \"#{e.message}\"; retrying for #{wait} more seconds.")
90 if wait > timeout - 2 # Only notice the first time
91 notice("#{service} service is unavailable. Will retry for up to #{wait} seconds.")
103 def authenticate_request(service, action, *args)
104 self.class.authenticate_request(service, action, *args)
109 def password_credentials_set?(auth_params)
110 auth_params && auth_params['username'] && auth_params['password'] && auth_params['tenant_name'] && auth_params['auth_url']
114 def openrc_set?(auth_params)
115 auth_params && auth_params['openrc']
119 def service_credentials_set?(auth_params)
120 auth_params && auth_params['token'] && auth_params['auth_url']
124 def self.env_vars_set?
125 ENV['OS_USERNAME'] && ENV['OS_PASSWORD'] && ENV['OS_TENANT_NAME'] && ENV['OS_AUTH_URL']
130 self.class.env_vars_set?
135 def self.password_auth_args(credentials)
136 ['--os-username', credentials['username'],
137 '--os-password', credentials['password'],
138 '--os-tenant-name', credentials['tenant_name'],
139 '--os-auth-url', credentials['auth_url']]
142 def password_auth_args(credentials)
143 self.class.password_auth_args(credentials)
147 def self.token_auth_args(credentials)
148 ['--os-token', credentials['token'],
149 '--os-url', credentials['auth_url']]
152 def token_auth_args(credentials)
153 self.class.token_auth_args(credentials)
156 def get_credentials_from_openrc(file)
158 File.open(file).readlines.delete_if{|l| l=~ /^#/}.each do |line|
159 key, value = line.split('=')
160 key = key.split(' ').last.downcase.sub(/^os_/, '')
161 value = value.chomp.gsub(/'/, '')
168 def self.get_credentials_from_env
169 env = ENV.to_hash.dup.delete_if { |key, _| ! (key =~ /^OS_/) }
171 env.each do |name, value|
172 credentials[name.downcase.sub(/^os_/, '')] = value
177 def get_credentials_from_env
178 self.class.get_credentials_from_env
181 def self.parse_csv(text)
182 # Ignore warnings - assume legitimate output starts with a double quoted
183 # string. Errors will be caught and raised prior to this
184 text = text.split("\n").drop_while { |line| line !~ /^\".*\"/ }.join("\n")
185 return CSV.parse(text + "\n")