]> git.donarmstrong.com Git - dsa-puppet.git/blob - 3rdparty/modules/aviator/lib/puppet/feature/aviator/core/session.rb
try again, with puppetforge modules, correctly included now
[dsa-puppet.git] / 3rdparty / modules / aviator / lib / puppet / feature / aviator / core / session.rb
1 #
2 # Author::    Mark Maglana (mmaglana@gmail.com)
3 # Copyright:: Copyright (c) 2014 Mark Maglana
4 # License::   Distributed under the MIT license
5 # Homepage::  http://aviator.github.io/www/
6 #
7 module Aviator
8
9   #
10   # Manages a provider (e.g. OpenStack) session and serves as the entry point
11   # for a consumer class/object. See Session::new for notes on usage.
12   #
13   class Session
14
15     class AuthenticationError < StandardError
16       def initialize(last_auth_body)
17         super("Authentication failed. The server returned #{ last_auth_body }")
18       end
19     end
20
21
22     class EnvironmentNotDefinedError < ArgumentError
23       def initialize(path, env)
24         super("The environment '#{ env }' is not defined in #{ path }.")
25       end
26     end
27
28     class InitializationError < StandardError
29       def initialize
30         super("The session could not find :session_dump, :config_file, and " \
31               ":config in the constructor arguments provided")
32       end
33     end
34
35     class InvalidConfigFilePathError < ArgumentError
36       def initialize(path)
37         super("The config file at #{ path } does not exist!")
38       end
39     end
40
41
42     class NotAuthenticatedError < StandardError
43       def initialize
44         super("Session is not authenticated. Please authenticate before proceeding.")
45       end
46     end
47
48
49     class ValidatorNotDefinedError < StandardError
50       def initialize
51         super("The validator request name is not defined for this session object.")
52       end
53     end
54
55     #
56     # Create a new Session instance.
57     #
58     # <b>Initialize with a config file</b>
59     #
60     #  Aviator::Session.new(:config_file => 'path/to/aviator.yml', :environment => :production)
61     #
62     # In the above example, the config file must have the following form:
63     #
64     #  production:
65     #    provider: openstack
66     #    auth_service:
67     #      name: identity
68     #      host_uri: 'http://my.openstackenv.org:5000'
69     #      request: create_token
70     #      validator: list_tenants
71     #      api_version: v2
72     #    auth_credentials:
73     #      username: myusername
74     #      password: mypassword
75     #      tenant_name: myproject
76     #
77     # <b>SIDENOTE:</b> For more information about the <tt>validator</tt> member, see Session#validate.
78     #
79     # Once the session has been instantiated, you may authenticate against the
80     # provider as follows:
81     #
82     #  session.authenticate
83     #
84     # The members you put under <tt>auth_credentials</tt> will depend on the request
85     # class you declare under <tt>auth_service:request</tt> and what parameters it
86     # accepts. To know more about a request class and its parameters, you can use
87     # the CLI tool <tt>aviator describe</tt> or view the request definition file directly.
88     #
89     # If writing the <tt>auth_credentials</tt> in the config file is not acceptable,
90     # you may omit it and just supply the credentials at runtime. For example:
91     #
92     #  session.authenticate do |params|
93     #    params.username    = ARGV[0]
94     #    params.password    = ARGV[1]
95     #    params.tenant_name = ARGV[2]
96     #  end
97     #
98     # See Session#authenticate for more info.
99     #
100     # Note that while the example config file above only has one environment (production),
101     # you can declare an arbitrary number of environments in your config file. Shifting
102     # between environments is as simple as changing the <tt>:environment</tt> to refer to that.
103     #
104     #
105     # <b>Initialize with an in-memory hash</b>
106     #
107     # You can create an in-memory hash with a structure similar to the config file but without
108     # the environment name. For example:
109     #
110     #  configuration = {
111     #    :provider => 'openstack',
112     #    :auth_service => {
113     #      :name      => 'identity',
114     #      :host_uri  => 'http://devstack:5000/v2.0',
115     #      :request   => 'create_token',
116     #      :validator => 'list_tenants'
117     #    }
118     #  }
119     #
120     # Supply this to the initializer using the <tt>:config</tt> option. For example:
121     #
122     #  Aviator::Session.new(:config => configuration)
123     #
124     #
125     # <b>Initialize with a session dump</b>
126     #
127     # You can create a new Session instance using a dump from another instance. For example:
128     #
129     #  session_dump = session1.dump
130     #  session2 = Aviator::Session.new(:session_dump => session_dump)
131     #
132     # However, Session.load is cleaner and recommended over this method.
133     #
134     #
135     # <b>Optionally supply a log file</b>
136     #
137     # In all forms above, you may optionally add a <tt>:log_file</tt> option to make
138     # Aviator write all HTTP calls to the given path. For example:
139     #
140     #  Aviator::Session.new(:config_file => 'path/to/aviator.yml', :environment => :production, :log_file => 'path/to/log')
141     #
142     def initialize(opts={})
143       if opts.has_key? :session_dump
144         initialize_with_dump(opts[:session_dump])
145       elsif opts.has_key? :config_file
146         initialize_with_config(opts[:config_file], opts[:environment])
147       elsif opts.has_key? :config
148         initialize_with_hash(opts[:config])
149       else
150         raise InitializationError.new
151       end
152
153       @log_file = opts[:log_file]
154     end
155
156     #
157     # Authenticates against the backend provider using the auth_service request class
158     # declared in the session's configuration. Please see Session.new for more information
159     # on declaring the request class to use for authentication.
160     #
161     # <b>Request params block</b>
162     #
163     # If the auth_service request class accepts parameters, you may supply that
164     # as a block and it will be directly passed to the request. For example:
165     #
166     #  session = Aviator::Session.new(:config => config)
167     #  session.authenticate do |params|
168     #    params.username    = username
169     #    params.password    = password
170     #    params.tenant_name = project
171     #  end
172     #
173     # If your configuration happens to have an <tt>auth_credentials</tt> in it, those
174     # will be overridden by this block.
175     #
176     # <b>Treat parameters as a hash</b>
177     #
178     # You can also treat the params struct like a hash with the attribute
179     # names as the keys. For example, we can rewrite the above as:
180     #
181     #  session = Aviator::Session.new(:config => config)
182     #  session.authenticate do |params|
183     #    params[:username]    = username
184     #    params[:password]    = password
185     #    params[:tenant_name] = project
186     #  end
187     #
188     # Keys can be symbols or strings.
189     #
190     # <b>Use a hash argument instead of a block</b>
191     #
192     # You may also provide request params as an argument instead of a block. This is
193     # especially useful if you want to mock Aviator as it's easier to specify ordinary
194     # argument expectations over blocks. Further rewriting the example above,
195     # we end up with:
196     #
197     #  session = Aviator::Session.new(:config => config)
198     #  session.authenticate :params => {
199     #    :username    => username,
200     #    :password    => password,
201     #    :tenant_name => project
202     #  }
203     #
204     # If both <tt>:params</tt> and a block are provided, the <tt>:params</tt>
205     # values will be used and the block ignored.
206     #
207     # <b>Success requirements</b>
208     #
209     # Expects an HTTP status 200 or 201 response from the backend. Any other
210     # status is treated as a failure.
211     #
212     def authenticate(opts={}, &block)
213       block ||= lambda do |params|
214         config[:auth_credentials].each do |key, value|
215           begin
216             params[key] = value
217           rescue NameError => e
218             raise NameError.new("Unknown param name '#{key}'")
219           end
220         end
221       end
222
223       response = auth_service.request(config[:auth_service][:request].to_sym, opts, &block)
224
225       if [200, 201].include? response.status
226         @auth_response = Hashish.new({
227           :headers => response.headers,
228           :body    => response.body
229         })
230         update_services_session_data
231       else
232         raise AuthenticationError.new(response.body)
233       end
234       self
235     end
236
237     #
238     # Returns true if the session has been authenticated. Note that this relies on
239     # cached response from a previous run of Session#authenticate if one was made.
240     # If you want to check against the backend provider if the session is still valid,
241     # use Session#validate instead.
242     #
243     def authenticated?
244       !auth_response.nil?
245     end
246
247     #
248     # Returns its configuration.
249     #
250     def config
251       @config
252     end
253
254     #
255     # Returns a JSON string of its configuration and auth_data. This string can be streamed
256     # or stored and later re-loaded in another Session instance. For example:
257     #
258     #  session = Aviator::Session.new(:config => configuration)
259     #  str = session.dump
260     #
261     #  # time passes...
262     #
263     #  session = Aviator::Session.load(str)
264     #
265     def dump
266       JSON.generate({
267         :config        => config,
268         :auth_response => auth_response
269       })
270     end
271
272
273     #
274     # Same as Session::load but re-uses the Session instance this method is
275     # called on instead of creating a new one.
276     #
277     def load(session_dump)
278       initialize_with_dump(session_dump)
279       update_services_session_data
280       self
281     end
282
283
284     def method_missing(name, *args, &block) # :nodoc:
285       service_name_parts = name.to_s.match(/^(\w+)_service$/)
286
287       if service_name_parts
288         get_service_obj(service_name_parts[1])
289       else
290         super name, *args, &block
291       end
292     end
293
294
295     #
296     # Creates a new Session object from a previous session's dump. See Session#dump for
297     # more information.
298     #
299     # If you want the newly deserialized session to log its output, add a <tt>:log_file</tt>
300     # option.
301     #
302     #  Aviator::Session.load(session_dump_str, :log_file => 'path/to/aviator.log')
303     #
304     def self.load(session_dump, opts={})
305       opts[:session_dump] = session_dump
306
307       new(opts)
308     end
309
310
311     #
312     # Returns the log file path. May be nil if none was provided during initialization.
313     #
314     def log_file
315       @log_file
316     end
317
318
319     #
320     # Calls the given request of the given service. An example call might look like:
321     #
322     #  session.request :compute_service, :create_server do |p|
323     #    p.name       = "My Server"
324     #    p.image_ref  = "7cae8c8e-fb01-4a88-bba3-ae0fcb1dbe29"
325     #    p.flavor_ref = "fa283da1-59a5-4245-8569-b6eadf69f10b"
326     #  end
327     #
328     # Note that you can also treat the block's argument like a hash with the attribute
329     # names as the keys. For example, we can rewrite the above as:
330     #
331     #  session.request :compute_service, :create_server do |p|
332     #    p[:name]       = "My Server"
333     #    p[:image_ref]  = "7cae8c8e-fb01-4a88-bba3-ae0fcb1dbe29"
334     #    p[:flavor_ref] = "fa283da1-59a5-4245-8569-b6eadf69f10b"
335     #  end
336     #
337     # Keys can be symbols or strings.
338     #
339     # You may also provide parameters as an argument instead of a block. This is
340     # especially useful when mocking Aviator as it's easier to specify ordinary
341     # argument expectations over blocks. Further rewriting the example above,
342     # we end up with:
343     #
344     #  session.request :compute_service, :create_server, :params => {
345     #    :name       => "My Server",
346     #    :image_ref  => "7cae8c8e-fb01-4a88-bba3-ae0fcb1dbe29",
347     #    :flavor_ref => "fa283da1-59a5-4245-8569-b6eadf69f10b"
348     #  }
349     #
350     # If both <tt>:params</tt> and a block are provided, the values in <tt>:params</tt>
351     # will be used and the block ignored.
352     #
353     # <b>Return Value</b>
354     #
355     # The return value will be an instance of Hashish, a lightweight replacement for
356     # activesupport's HashWithIndifferentAccess, with the following structure:
357     #
358     #   {
359     #     :status => 200,
360     #     :headers => {
361     #       'X-Auth-Token' => 'd9186f45ce5446eaa0adc9def1c46f5f',
362     #       'Content-Type' => 'application/json'
363     #     },
364     #     :body => {
365     #       :some_key => :some_value
366     #     }
367     #   }
368     #
369     # Note that the members in <tt>:headers</tt> and <tt>:body</tt> will vary depending
370     # on the provider and the request that was made.
371     #
372     # ---
373     #
374     # <b>Request Options</b>
375     #
376     # You can further customize how the method behaves by providing one or more
377     # options to the call. For example, assuming you are using the <tt>openstack</tt>
378     # provider, the following will call the <tt>:create_server</tt> request of the
379     # v1 API of <tt>:compute_service</tt>.
380     #
381     #  session.request :compute_service, :create_server, :api_version => v1, :params => params
382     #
383     # The available options vary depending on the provider. See the documentation
384     # on the provider's Provider class for more information (e.g. Aviator::Openstack::Provider)
385     #
386     def request(service_name, request_name, opts={}, &block)
387       service = send("#{service_name.to_s}_service")
388       response = service.request(request_name, opts, &block)
389       response.to_hash
390     end
391
392
393     #
394     # Returns true if the session is still valid in the underlying provider. This method calls
395     # the <tt>validator</tt> request class declared under <tt>auth_service</tt> in the
396     # configuration. The validator can be any request class as long as:
397     #
398     # * The request class exists!
399     # * Is not an anonymous request. Otherwise it will always return true.
400     # * Does not require any parameters
401     # * It returns an HTTP status 200 or 203 to indicate auth info validity.
402     # * It returns any other HTTP status to indicate that the auth info is invalid.
403     #
404     # See Session::new for an example on how to specify the request class to use for session validation.
405     #
406     # Note that this method requires the session to be previously authenticated otherwise a
407     # NotAuthenticatedError will be raised. If you just want to check if the session was previously
408     # authenticated, use Session#authenticated? instead.
409     #
410     def validate
411       raise NotAuthenticatedError.new unless authenticated?
412       raise ValidatorNotDefinedError.new unless config[:auth_service][:validator]
413
414       auth_with_bootstrap = auth_response.merge({ :auth_service  => config[:auth_service] })
415
416       response = auth_service.request config[:auth_service][:validator].to_sym, :session_data => auth_with_bootstrap
417       response.status == 200 || response.status == 203
418     end
419
420
421     private
422
423
424     def auth_response
425       @auth_response
426     end
427
428
429     def auth_service
430       @auth_service ||= Service.new(
431         :provider             => config[:provider],
432         :service              => config[:auth_service][:name],
433         :default_session_data => { :auth_service => config[:auth_service] },
434         :log_file             => log_file
435       )
436     end
437
438
439     def get_service_obj(service_name)
440       @services ||= {}
441
442       if @services[service_name].nil?
443         default_options = config["#{ service_name }_service"]
444
445         @services[service_name] = Service.new(
446           :provider             => config[:provider],
447           :service              => service_name,
448           :default_session_data => auth_response,
449           :default_options      => default_options,
450           :log_file             => log_file
451         )
452       end
453
454       @services[service_name]
455     end
456
457
458     def initialize_with_config(config_path, environment)
459       raise InvalidConfigFilePathError.new(config_path) unless Pathname.new(config_path).file?
460
461       all_config = Hashish.new(YAML.load_file(config_path))
462
463       raise EnvironmentNotDefinedError.new(config_path, environment) unless all_config[environment]
464
465       @config = all_config[environment]
466     end
467
468
469     def initialize_with_dump(session_dump)
470       session_info   = Hashish.new(JSON.parse(session_dump))
471       @config        = session_info[:config]
472       @auth_response = session_info[:auth_response]
473     end
474
475
476     def initialize_with_hash(hash_obj)
477       @config = Hashish.new(hash_obj)
478     end
479
480
481     def update_services_session_data
482       return unless @services
483
484       @services.each do |name, obj|
485         obj.default_session_data = auth_response
486       end
487     end
488
489   end
490
491 end