]> git.donarmstrong.com Git - dsa-puppet.git/blob - 3rdparty/modules/keystone/lib/puppet/provider/keystone_user/openstack.rb
6c8d04aa292f7d9bee447cba712986590921abd2
[dsa-puppet.git] / 3rdparty / modules / keystone / lib / puppet / provider / keystone_user / openstack.rb
1 require 'net/http'
2 require 'json'
3 require 'puppet/provider/keystone'
4 Puppet::Type.type(:keystone_user).provide(
5   :openstack,
6   :parent => Puppet::Provider::Keystone
7 ) do
8
9   desc "Provider to manage keystone users."
10
11   def initialize(value={})
12     super(value)
13     @property_flush = {}
14   end
15
16   def create
17     properties = []
18     if resource[:enabled] == :true
19       properties << '--enable'
20     elsif resource[:enabled] == :false
21       properties << '--disable'
22     end
23     if resource[:password]
24       properties << '--password'
25       properties << resource[:password]
26     end
27     if resource[:tenant]
28       properties << '--project'
29       properties << resource[:tenant]
30     end
31     if resource[:email]
32       properties << '--email'
33       properties << resource[:email]
34     end
35     @instance = request('user', 'create', resource[:name], resource[:auth], properties)
36   end
37
38   def exists?
39     ! instance(resource[:name]).empty?
40   end
41
42   def destroy
43     request('user', 'delete', resource[:name], resource[:auth])
44   end
45
46
47   def enabled=(value)
48     @property_flush[:enabled] = value
49   end
50
51   def enabled
52     bool_to_sym(instance(resource[:name])[:enabled])
53   end
54
55
56   def password=(value)
57     @property_flush[:password] = value
58   end
59
60   def password
61     # if we don't know a password we can't test it
62     return nil if resource[:password] == nil
63     # if the user is disabled then the password can't be changed
64     return resource[:password] if resource[:enabled] == :false
65     # if replacing password is disabled, then don't change it
66     return resource[:password] if resource[:replace_password] == :false
67     # we can't get the value of the password but we can test to see if the one we know
68     # about works, if it doesn't then return nil, causing it to be reset
69     endpoint = nil
70     if password_credentials_set?(resource[:auth]) || service_credentials_set?(resource[:auth])
71       endpoint = (resource[:auth])['auth_url']
72     elsif openrc_set?(resource[:auth])
73       endpoint = get_credentials_from_openrc(resource[:auth])['auth_url']
74     elsif env_vars_set?
75       endpoint = ENV['OS_AUTH_URL']
76     else
77       # try to get endpoint from keystone.conf
78       endpoint = get_admin_endpoint
79     end
80     if endpoint == nil
81       raise(Puppet::Error::OpenstackAuthInputError, 'Could not find auth url to check user password.')
82     else
83       auth_params = {
84         'username'    => resource[:name],
85         'password'    => resource[:password],
86         'tenant_name' => resource[:tenant],
87         'auth_url'    => endpoint,
88       }
89       # LP#1408754
90       # Ideally this would be checked with the `openstack token issue` command,
91       # but that command is not available with version 0.3.0 of openstackclient
92       # which is what ships on Ubuntu during Juno.
93       # Instead we'll check whether the user can authenticate with curl.
94       creds_hash = {
95         :auth => {
96           :passwordCredentials => {
97             :username => auth_params['username'],
98             :password => auth_params['password'],
99           }
100         }
101       }
102       url = URI.parse(endpoint)
103       # There is issue with ipv6 where address has to be in brackets, this causes the
104       # underlying ruby TCPSocket to fail. Net::HTTP.new will fail without brackets on
105       # joining the ipv6 address with :port or passing brackets to TCPSocket. It was
106       # found that if we use Net::HTTP.start with url.hostname the incriminated code
107       # won't be hit.
108       use_ssl = url.scheme == "https" ? true : false
109       http = Net::HTTP.start(url.hostname, url.port, {:use_ssl => use_ssl})
110       request = Net::HTTP::Post.new('/v2.0/tokens')
111       request.body = creds_hash.to_json
112       request.content_type = 'application/json'
113       response = http.request(request)
114       if response.code.to_i == 401 || response.code.to_i == 403 # 401 => unauthorized, 403 => userDisabled
115         return nil
116       elsif ! (response.code == 200 || response.code == 203)
117         return resource[:password]
118       else
119         raise(Puppet::Error, "Received bad response while trying to authenticate user: #{response.body}")
120       end
121     end
122   end
123
124   def tenant=(value)
125     begin
126       request('user', 'set', resource[:name], resource[:auth], '--project', value)
127     rescue Puppet::ExecutionFailure => e
128       if e.message =~ /You are not authorized to perform the requested action: LDAP user update/
129         # read-only LDAP identity backend - just fall through
130       else
131         raise e
132       end
133       # note: read-write ldap will silently fail, not raise an exception
134     end
135     set_project(value)
136   end
137
138   def tenant
139     return resource[:tenant] if sym_to_bool(resource[:ignore_default_tenant])
140     # use the one returned from instances
141     tenant_name = instance(resource[:name])[:project]
142     if tenant_name.nil? or tenant_name.empty?
143       # if none (i.e. ldap backend) use the given one
144       tenant_name = resource[:tenant]
145     else
146       return tenant_name
147     end
148     if tenant_name.nil? or tenant_name.empty?
149       return nil # nothing found, nothing given
150     end
151     # If the user list command doesn't report the project, it might still be there
152     # We don't need to know exactly what it is, we just need to know whether it's
153     # the one we're trying to set.
154     roles = request('user role', 'list', resource[:name], resource[:auth], ['--project', tenant_name])
155     if roles.empty?
156       return nil
157     else
158       return tenant_name
159     end
160   end
161
162   def replace_password
163     instance(resource[:name])[:replace_password]
164   end
165
166   def replace_password=(value)
167     @property_flush[:replace_password] = value
168   end
169
170   def email=(value)
171     @property_flush[:email] = value
172   end
173
174   def email
175     instance(resource[:name])[:email]
176   end
177
178   def id
179     instance(resource[:name])[:id]
180   end
181
182   def self.instances
183     list = request('user', 'list', nil, nil, '--long')
184     list.collect do |user|
185       new(
186         :name        => user[:name],
187         :ensure      => :present,
188         :enabled     => user[:enabled].downcase.chomp == 'true' ? true : false,
189         :password    => user[:password],
190         :tenant      => user[:project],
191         :email       => user[:email],
192         :id          => user[:id]
193       )
194     end
195   end
196
197   def instances
198     instances = request('user', 'list', nil, resource[:auth], '--long')
199     instances.collect do |user|
200       {
201         :name        => user[:name],
202         :enabled     => user[:enabled].downcase.chomp == 'true' ? true : false,
203         :password    => user[:password],
204         :project     => user[:project],
205         :email       => user[:email],
206         :id          => user[:id]
207       }
208     end
209   end
210
211   def instance(name)
212     @instance ||= instances.select { |instance| instance[:name] == name }.first || {}
213   end
214
215   def set_project(newproject)
216     # some backends do not store the project/tenant in the user object, so we have to
217     # to modify the project/tenant instead
218     # First, see if the project actually needs to change
219     roles = request('user role', 'list', resource[:name], resource[:auth], ['--project', newproject])
220     unless roles.empty?
221       return # if already set, just skip
222     end
223     # Currently the only way to assign a user to a tenant not using user-create
224     # is to use user-role-add - this means we also need a role - there is usual
225     # a default role called _member_ which can be used for this purpose.  What
226     # usually happens in a puppet module is that immediately after calling
227     # keystone_user, the module will then assign a role to that user.  It is
228     # ok for a user to have the _member_ role and another role.
229     default_role = "_member_"
230     begin
231       request('role', 'show', default_role, resource[:auth])
232     rescue
233       debug("Keystone role #{default_role} does not exist - creating")
234       request('role', 'create', default_role, resource[:auth])
235     end
236     request('role', 'add', default_role, resource[:auth],
237             '--project', newproject, '--user', resource[:name])
238   end
239
240   def flush
241     options = []
242     if @property_flush
243       (options << '--enable') if @property_flush[:enabled] == :true
244       (options << '--disable') if @property_flush[:enabled] == :false
245       # There is a --description flag for the set command, but it does not work if the value is empty
246       (options << '--password' << resource[:password]) if @property_flush[:password]
247       (options << '--email'    << resource[:email])    if @property_flush[:email]
248       # project handled in tenant= separately
249       request('user', 'set', resource[:name], resource[:auth], options) unless options.empty?
250     end
251   end
252
253 end