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/
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.
15 class AuthenticationError < StandardError
16 def initialize(last_auth_body)
17 super("Authentication failed. The server returned #{ last_auth_body }")
22 class EnvironmentNotDefinedError < ArgumentError
23 def initialize(path, env)
24 super("The environment '#{ env }' is not defined in #{ path }.")
28 class InitializationError < StandardError
30 super("The session could not find :session_dump, :config_file, and " \
31 ":config in the constructor arguments provided")
35 class InvalidConfigFilePathError < ArgumentError
37 super("The config file at #{ path } does not exist!")
42 class NotAuthenticatedError < StandardError
44 super("Session is not authenticated. Please authenticate before proceeding.")
49 class ValidatorNotDefinedError < StandardError
51 super("The validator request name is not defined for this session object.")
56 # Create a new Session instance.
58 # <b>Initialize with a config file</b>
60 # Aviator::Session.new(:config_file => 'path/to/aviator.yml', :environment => :production)
62 # In the above example, the config file must have the following form:
68 # host_uri: 'http://my.openstackenv.org:5000'
69 # request: create_token
70 # validator: list_tenants
73 # username: myusername
74 # password: mypassword
75 # tenant_name: myproject
77 # <b>SIDENOTE:</b> For more information about the <tt>validator</tt> member, see Session#validate.
79 # Once the session has been instantiated, you may authenticate against the
80 # provider as follows:
82 # session.authenticate
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.
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:
92 # session.authenticate do |params|
93 # params.username = ARGV[0]
94 # params.password = ARGV[1]
95 # params.tenant_name = ARGV[2]
98 # See Session#authenticate for more info.
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.
105 # <b>Initialize with an in-memory hash</b>
107 # You can create an in-memory hash with a structure similar to the config file but without
108 # the environment name. For example:
111 # :provider => 'openstack',
113 # :name => 'identity',
114 # :host_uri => 'http://devstack:5000/v2.0',
115 # :request => 'create_token',
116 # :validator => 'list_tenants'
120 # Supply this to the initializer using the <tt>:config</tt> option. For example:
122 # Aviator::Session.new(:config => configuration)
125 # <b>Initialize with a session dump</b>
127 # You can create a new Session instance using a dump from another instance. For example:
129 # session_dump = session1.dump
130 # session2 = Aviator::Session.new(:session_dump => session_dump)
132 # However, Session.load is cleaner and recommended over this method.
135 # <b>Optionally supply a log file</b>
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:
140 # Aviator::Session.new(:config_file => 'path/to/aviator.yml', :environment => :production, :log_file => 'path/to/log')
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])
150 raise InitializationError.new
153 @log_file = opts[:log_file]
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.
161 # <b>Request params block</b>
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:
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
173 # If your configuration happens to have an <tt>auth_credentials</tt> in it, those
174 # will be overridden by this block.
176 # <b>Treat parameters as a hash</b>
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:
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
188 # Keys can be symbols or strings.
190 # <b>Use a hash argument instead of a block</b>
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,
197 # session = Aviator::Session.new(:config => config)
198 # session.authenticate :params => {
199 # :username => username,
200 # :password => password,
201 # :tenant_name => project
204 # If both <tt>:params</tt> and a block are provided, the <tt>:params</tt>
205 # values will be used and the block ignored.
207 # <b>Success requirements</b>
209 # Expects an HTTP status 200 or 201 response from the backend. Any other
210 # status is treated as a failure.
212 def authenticate(opts={}, &block)
213 block ||= lambda do |params|
214 config[:auth_credentials].each do |key, value|
217 rescue NameError => e
218 raise NameError.new("Unknown param name '#{key}'")
223 response = auth_service.request(config[:auth_service][:request].to_sym, opts, &block)
225 if [200, 201].include? response.status
226 @auth_response = Hashish.new({
227 :headers => response.headers,
228 :body => response.body
230 update_services_session_data
232 raise AuthenticationError.new(response.body)
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.
248 # Returns its configuration.
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:
258 # session = Aviator::Session.new(:config => configuration)
263 # session = Aviator::Session.load(str)
268 :auth_response => auth_response
274 # Same as Session::load but re-uses the Session instance this method is
275 # called on instead of creating a new one.
277 def load(session_dump)
278 initialize_with_dump(session_dump)
279 update_services_session_data
284 def method_missing(name, *args, &block) # :nodoc:
285 service_name_parts = name.to_s.match(/^(\w+)_service$/)
287 if service_name_parts
288 get_service_obj(service_name_parts[1])
290 super name, *args, &block
296 # Creates a new Session object from a previous session's dump. See Session#dump for
299 # If you want the newly deserialized session to log its output, add a <tt>:log_file</tt>
302 # Aviator::Session.load(session_dump_str, :log_file => 'path/to/aviator.log')
304 def self.load(session_dump, opts={})
305 opts[:session_dump] = session_dump
312 # Returns the log file path. May be nil if none was provided during initialization.
320 # Calls the given request of the given service. An example call might look like:
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"
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:
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"
337 # Keys can be symbols or strings.
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,
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"
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.
353 # <b>Return Value</b>
355 # The return value will be an instance of Hashish, a lightweight replacement for
356 # activesupport's HashWithIndifferentAccess, with the following structure:
361 # 'X-Auth-Token' => 'd9186f45ce5446eaa0adc9def1c46f5f',
362 # 'Content-Type' => 'application/json'
365 # :some_key => :some_value
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.
374 # <b>Request Options</b>
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>.
381 # session.request :compute_service, :create_server, :api_version => v1, :params => params
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)
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)
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:
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.
404 # See Session::new for an example on how to specify the request class to use for session validation.
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.
411 raise NotAuthenticatedError.new unless authenticated?
412 raise ValidatorNotDefinedError.new unless config[:auth_service][:validator]
414 auth_with_bootstrap = auth_response.merge({ :auth_service => config[:auth_service] })
416 response = auth_service.request config[:auth_service][:validator].to_sym, :session_data => auth_with_bootstrap
417 response.status == 200 || response.status == 203
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
439 def get_service_obj(service_name)
442 if @services[service_name].nil?
443 default_options = config["#{ service_name }_service"]
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
454 @services[service_name]
458 def initialize_with_config(config_path, environment)
459 raise InvalidConfigFilePathError.new(config_path) unless Pathname.new(config_path).file?
461 all_config = Hashish.new(YAML.load_file(config_path))
463 raise EnvironmentNotDefinedError.new(config_path, environment) unless all_config[environment]
465 @config = all_config[environment]
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]
476 def initialize_with_hash(hash_obj)
477 @config = Hashish.new(hash_obj)
481 def update_services_session_data
482 return unless @services
484 @services.each do |name, obj|
485 obj.default_session_data = auth_response