occi-api 4.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. data/.gitignore +15 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +43 -0
  4. data/.yardopts +1 -0
  5. data/AUTHORS +9 -0
  6. data/Gemfile +16 -0
  7. data/LICENSE +13 -0
  8. data/README.md +344 -0
  9. data/Rakefile +37 -0
  10. data/examples/dsl_example.rb +167 -0
  11. data/examples/x509auth_example.rb +161 -0
  12. data/ext/mkrf_conf.rb +34 -0
  13. data/features/cassettes/Create_an_OCCI_Resource/_http_http___141_5_99_69__text_plain_201_.yml +288 -0
  14. data/features/cassettes/Delete_an_OCCI_Resource/_http_http___141_5_99_69__text_plain_201_.yml +288 -0
  15. data/features/cassettes/Discovery_Interface/Retrieving_all_OCCI_Categories_supported_by_the_OCCI_Server/_http_http___141_5_99_69__application_json_200_.yml +333 -0
  16. data/features/cassettes/Discovery_Interface/Retrieving_all_OCCI_Categories_supported_by_the_OCCI_Server/_http_http___141_5_99_69__text_plain_200_.yml +529 -0
  17. data/features/cassettes/Discovery_Interface/Retrieving_all_OCCI_Categories_supported_by_the_OCCI_Server/_http_http___141_5_99_69__text_plain_200_action_.yml +288 -0
  18. data/features/cassettes/Miscellaneous_operation_on_an_OCCI_Resource/_http_http___141_5_99_69__text_plain_201_.yml +288 -0
  19. data/features/cassettes/Read_an_OCCI_Resource/_http_http___141_5_99_69__text_plain_201_.yml +288 -0
  20. data/features/cassettes/Update_an_OCCI_Resource/_http_http___141_5_99_69__text_plain_201_.yml +288 -0
  21. data/features/common/step_definitions/common_steps.rb +32 -0
  22. data/features/occi/core/create/create.feature +18 -0
  23. data/features/occi/core/create/step_definitions/create_steps.rb +0 -0
  24. data/features/occi/core/delete/delete.feature +18 -0
  25. data/features/occi/core/delete/step_definitions/delete_steps.rb +0 -0
  26. data/features/occi/core/discovery_interface/discovery_interface.feature +37 -0
  27. data/features/occi/core/discovery_interface/step_definitions/discovery_interface_steps.rb +19 -0
  28. data/features/occi/core/miscellaneous/miscellaneous.feature +18 -0
  29. data/features/occi/core/miscellaneous/step_definitions/miscellaneous_steps.rb +0 -0
  30. data/features/occi/core/read/read.feature +18 -0
  31. data/features/occi/core/read/step_definitions/read_steps.rb +0 -0
  32. data/features/occi/core/update/step_definitions/update_steps.rb +0 -0
  33. data/features/occi/core/update/update.feature +18 -0
  34. data/features/occi/infrastructure/create/create.feature +18 -0
  35. data/features/occi/infrastructure/create/step_definitions/create_steps.rb +0 -0
  36. data/features/support/env.rb +16 -0
  37. data/lib/occi/api/client/client_amqp.rb +766 -0
  38. data/lib/occi/api/client/client_base.rb +689 -0
  39. data/lib/occi/api/client/client_http.rb +541 -0
  40. data/lib/occi/api/client/errors/authn_error.rb +7 -0
  41. data/lib/occi/api/client/errors.rb +1 -0
  42. data/lib/occi/api/client/http/authn_plugins/base.rb +27 -0
  43. data/lib/occi/api/client/http/authn_plugins/basic.rb +22 -0
  44. data/lib/occi/api/client/http/authn_plugins/digest.rb +22 -0
  45. data/lib/occi/api/client/http/authn_plugins/dummy.rb +13 -0
  46. data/lib/occi/api/client/http/authn_plugins/keystone.rb +61 -0
  47. data/lib/occi/api/client/http/authn_plugins/x509.rb +46 -0
  48. data/lib/occi/api/client/http/authn_plugins.rb +6 -0
  49. data/lib/occi/api/client/http/authn_utils.rb +87 -0
  50. data/lib/occi/api/client/http/httparty_fix.rb +53 -0
  51. data/lib/occi/api/client/http/net_http_fix.rb +21 -0
  52. data/lib/occi/api/dsl.rb +146 -0
  53. data/lib/occi/api/version.rb +5 -0
  54. data/lib/occi-api.rb +14 -0
  55. data/occi-api.gemspec +38 -0
  56. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/creates_a_new_compute_resource.yml +266 -0
  57. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/creates_a_new_network_resource.yml +266 -0
  58. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/creates_a_new_storage_resource.yml +266 -0
  59. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/deletes_a_compute_resource.yml +266 -0
  60. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/deletes_a_network_resource.yml +266 -0
  61. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/deletes_a_storage_resource.yml +266 -0
  62. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/deploys_an_instance_based_on_OVF_OVA_file.yml +266 -0
  63. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/describes_compute_resources.yml +368 -0
  64. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/describes_network_resources.yml +370 -0
  65. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/describes_storage_resources.yml +430 -0
  66. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/establishes_connection.yml +266 -0
  67. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/finds_and_describes_scoped_os_tpl_mixin.yml +266 -0
  68. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/finds_and_describes_scoped_resource_tpl_mixin.yml +266 -0
  69. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/finds_and_describes_unscoped_mixin.yml +266 -0
  70. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/instantiates_a_compute_resource_using_type_identifier.yml +266 -0
  71. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/instantiates_a_compute_resource_using_type_name.yml +266 -0
  72. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/instantiates_a_network_resource_using_type_identifier.yml +266 -0
  73. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/instantiates_a_network_resource_using_type_name.yml +266 -0
  74. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/instantiates_a_storage_resource_using_type_identifier.yml +266 -0
  75. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/instantiates_a_storage_resource_using_type_name.yml +266 -0
  76. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_all_available_entity_type_identifiers.yml +266 -0
  77. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_all_available_entity_types.yml +266 -0
  78. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_all_available_link_type_identifiers.yml +266 -0
  79. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_all_available_link_types.yml +266 -0
  80. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_all_available_mixin_type_identifiers.yml +266 -0
  81. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_all_available_mixin_types.yml +266 -0
  82. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_all_available_mixins.yml +266 -0
  83. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_all_available_resource_type_identifiers.yml +266 -0
  84. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_all_available_resource_types.yml +266 -0
  85. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_compute_resources.yml +308 -0
  86. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_network_resources.yml +308 -0
  87. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_os_tpl_mixins.yml +266 -0
  88. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_resource_tpl_mixins.yml +266 -0
  89. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/lists_storage_resources.yml +310 -0
  90. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/refreshes_its_model.yml +485 -0
  91. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/triggers_an_action_on_a_compute_resource.yml +266 -0
  92. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/triggers_an_action_on_a_network_resource.yml +266 -0
  93. data/spec/cassettes/Occi_Api_Client_ClientHttp/using_media_type_text_plain/triggers_an_action_on_a_storage_resource.yml +266 -0
  94. data/spec/occi/api/client/client_amqp_spec.rb +158 -0
  95. data/spec/occi/api/client/client_http_spec.rb +292 -0
  96. data/spec/occi/api/client/http/authn_utils_spec.rb +55 -0
  97. data/spec/occi/api/client/http/httparty_fix_spec.rb +0 -0
  98. data/spec/occi/api/client/http/net_http_fix_spec.rb +0 -0
  99. data/spec/occi/api/client/http/rocci-cred-cert.pem +3 -0
  100. data/spec/occi/api/client/http/rocci-cred-key-jruby.pem +3 -0
  101. data/spec/occi/api/client/http/rocci-cred-key.pem +3 -0
  102. data/spec/occi/api/client/http/rocci-cred.p12 +0 -0
  103. data/spec/occi/api/dsl_spec.rb +21 -0
  104. data/spec/spec_helper.rb +38 -0
  105. metadata +379 -0
@@ -0,0 +1,541 @@
1
+ require 'httparty'
2
+
3
+ require 'occi/api/client/http/net_http_fix'
4
+ require 'occi/api/client/http/httparty_fix'
5
+ require 'occi/api/client/http/authn_utils'
6
+
7
+ module Occi
8
+ module Api
9
+ module Client
10
+
11
+ class ClientHttp < ClientBase
12
+
13
+ # HTTParty for raw HTTP requests
14
+ include HTTParty
15
+
16
+ # TODO: uncomment the following line as JSON is properly implemented in OpenStack
17
+ # headers 'Accept' => 'application/occi+json,text/plain;q=0.8,text/occi;q=0.2'
18
+ headers 'Accept' => 'text/plain,text/occi;q=0.2'
19
+
20
+ # hash mapping HTTP response codes to human-readable messages
21
+ HTTP_CODES = {
22
+ "100" => "Continue",
23
+ "101" => "Switching Protocols",
24
+ "200" => "OK",
25
+ "201" => "Created",
26
+ "202" => "Accepted",
27
+ "203" => "Non-Authoritative Information",
28
+ "204" => "No Content",
29
+ "205" => "Reset Content",
30
+ "206" => "Partial Content",
31
+ "300" => "Multiple Choices",
32
+ "301" => "Moved Permanently",
33
+ "302" => "Found",
34
+ "303" => "See Other",
35
+ "304" => "Not Modified",
36
+ "305" => "Use Proxy",
37
+ "307" => "Temporary Redirect",
38
+ "400" => "Bad Request",
39
+ "401" => "Unauthorized",
40
+ "402" => "Payment Required",
41
+ "403" => "Forbidden",
42
+ "404" => "Not Found",
43
+ "405" => "Method Not Allowed",
44
+ "406" => "Not Acceptable",
45
+ "407" => "Proxy Authentication Required",
46
+ "408" => "Request Time-out",
47
+ "409" => "Conflict",
48
+ "410" => "Gone",
49
+ "411" => "Length Required",
50
+ "412" => "Precondition Failed",
51
+ "413" => "Request Entity Too Large",
52
+ "414" => "Request-URI Too Large",
53
+ "415" => "Unsupported Media Type",
54
+ "416" => "Requested range not satisfiable",
55
+ "417" => "Expectation Failed",
56
+ "500" => "Internal Server Error",
57
+ "501" => "Not Implemented",
58
+ "502" => "Bad Gateway",
59
+ "503" => "Service Unavailable",
60
+ "504" => "Gateway Time-out",
61
+ "505" => "HTTP Version not supported"
62
+ }
63
+
64
+ # Initializes client data structures and retrieves OCCI model
65
+ # from the server.
66
+ #
67
+ # @example
68
+ # options = {
69
+ # :endpoint => "http://localhost:3300/",
70
+ # :auth => {:type => "none"},
71
+ # :log => {:out => STDERR, :level => Occi::Log::WARN, :logger => nil},
72
+ # :auto_connect => "value", auto_connect => true,
73
+ # :media_type => nil
74
+ # }
75
+ #
76
+ # Occi::Api::Client::ClientHttp.new options # => #<Occi::Api::Client::ClientHttp>
77
+ #
78
+ # @param [Hash] options, for available options and defaults see examples
79
+ # @return [Occi::Api::Client::ClientHttp] client instance
80
+ def initialize(options = {})
81
+ super options
82
+
83
+ # get model information from the endpoint
84
+ # and create Occi::Model instance
85
+ model = get('/-/')
86
+ set_model model
87
+
88
+ # auto-connect?
89
+ @connected = @options[:auto_connect]
90
+ end
91
+
92
+ # @see Occi::Api::Client::ClientBase
93
+ def list(resource_type_identifier=nil)
94
+ if resource_type_identifier
95
+ # convert type to type identifier
96
+ kinds = @model.kinds.select {
97
+ |kind| kind.term == resource_type_identifier
98
+ }
99
+ if kinds.any?
100
+ resource_type_identifier = kinds.first.type_identifier
101
+ end
102
+
103
+ raise 'Unkown resource type identifier!' unless resource_type_identifier
104
+ unless @model.get_by_id resource_type_identifier
105
+ raise "Resource type identifier not allowed with this model! [#{resource_type_identifier}]"
106
+ end
107
+
108
+ # split the type identifier and get the most important part
109
+ uri_part = resource_type_identifier.split('#').last
110
+
111
+ # request uri-list from the server
112
+ path = uri_part + '/'
113
+ end
114
+
115
+ path = '/' unless path
116
+
117
+ headers = self.class.headers.clone
118
+ headers['Accept'] = 'text/uri-list'
119
+
120
+ response = self.class.get(
121
+ @endpoint + path,
122
+ :headers => headers
123
+ )
124
+
125
+ # TODO: remove the gsub OCCI-OS hack as soon as they stop using 'uri:'
126
+ response.body.gsub(/\# uri:\/(compute|storage|network)\/[\n]?/, '').split("\n").compact
127
+ end
128
+
129
+ # @see Occi::Api::Client::ClientBase
130
+ def describe(resource_type_identifier=nil)
131
+
132
+ # convert type to type identifier whenever possible
133
+ if resource_type_identifier
134
+ kinds = @model.kinds.select {
135
+ |kind| kind.term == resource_type_identifier
136
+ }
137
+ if kinds.any?
138
+ resource_type_identifier = kinds.first.type_identifier
139
+ end
140
+ end
141
+
142
+ descriptions = []
143
+
144
+ if resource_type_identifier.nil?
145
+ # no filters, describe all available resources
146
+ descriptions << get('/')
147
+ elsif @model.get_by_id(resource_type_identifier)
148
+ # we got type identifier
149
+ # get all available resources of this type
150
+ locations = list(resource_type_identifier)
151
+
152
+ # make the requests
153
+ locations.each do |location|
154
+ descriptions << get(sanitize_resource_link(location))
155
+ end
156
+ elsif resource_type_identifier.start_with?(@endpoint) || resource_type_identifier.start_with?('/')
157
+ # this is a link of a specific resource (obsolute or relative)
158
+ descriptions << get(sanitize_resource_link(resource_type_identifier))
159
+ else
160
+ raise "Unkown resource type identifier! [#{resource_type_identifier}]"
161
+ end
162
+
163
+ descriptions
164
+ end
165
+
166
+ # @see Occi::Api::Client::ClientBase
167
+ def create(entity)
168
+
169
+ raise "#{entity} not an entity!" unless entity.kind_of? Occi::Core::Entity
170
+
171
+ # is this entity valid?
172
+ entity.model = @model
173
+ entity.check
174
+
175
+ Occi::Log.debug "Entity kind: #{entity.kind}"
176
+ kind = entity.kind
177
+ raise "No kind found for #{entity}" unless kind
178
+
179
+ # get location for this kind of entity
180
+ Occi::Log.debug "Kind location: #{entity.kind.location}"
181
+ location = kind.location
182
+ collection = Occi::Collection.new
183
+
184
+ # is this entity a Resource or a Link?
185
+ Occi::Log.debug "Entity class: #{entity.class.name}"
186
+ collection.resources << entity if entity.kind_of? Occi::Core::Resource
187
+ collection.links << entity if entity.kind_of? Occi::Core::Link
188
+
189
+ # make the request
190
+ post location, collection
191
+ end
192
+
193
+ # @see Occi::Api::Client::ClientBase
194
+ def deploy(location)
195
+ raise "File #{location} does not exist!" unless File.exist? location
196
+
197
+ file = File.read(location)
198
+
199
+ if location.include? '.ovf'
200
+ deploy_ovf file
201
+ elsif location.include? '.ova'
202
+ deploy_ova file
203
+ else
204
+ raise "Unsupported descriptor format! Only OVF or OVA files are supported."
205
+ end
206
+ end
207
+
208
+ # @see Occi::Api::Client::ClientBase
209
+ def deploy_ovf(descriptor)
210
+ media_types = self.class.head(@endpoint).headers['accept'].to_s
211
+
212
+ if media_types.include? 'application/ovf'
213
+ headers = self.class.headers.clone
214
+ headers['Content-Type'] = 'application/ovf'
215
+ self.class.post(@endpoint + '/compute/',
216
+ :body => descriptor,
217
+ :headers => headers)
218
+ else
219
+ raise "Unsupported descriptor format! Server does not support OVF descriptors."
220
+ end
221
+ end
222
+
223
+ # @see Occi::Api::Client::ClientBase
224
+ def deploy_ova(descriptor)
225
+ media_types = self.class.head(@endpoint).headers['accept'].to_s
226
+
227
+ if media_types.include? ' application/ova '
228
+ headers = self.class.headers.clone
229
+ headers['Content-Type'] = 'application/ova'
230
+ self.class.post(@endpoint + '/compute/',
231
+ :body => descriptor,
232
+ :headers => headers)
233
+ else
234
+ raise "Unsupported descriptor format! Server does not support OVA descriptors."
235
+ end
236
+ end
237
+
238
+ # @see Occi::Api::Client::ClientBase
239
+ def delete(resource_type_identifier)
240
+ raise 'Resource not provided!' unless resource_type_identifier
241
+ path = path_for_resource_type(resource_type_identifier)
242
+
243
+ Occi::Log.debug("Deleting #{path} ...")
244
+ del path
245
+ end
246
+
247
+ # @see Occi::Api::Client::ClientBase
248
+ def trigger(resource_type_identifier, action)
249
+ # TODO: not tested
250
+ raise 'Resource not provided!' unless resource_type_identifier
251
+ type_identifiers = @model.kinds.select {
252
+ |kind| kind.term == resource_type_identifier
253
+ }
254
+
255
+ if type_identifiers.any?
256
+ type_identifier = @model.kinds.select {
257
+ |kind| kind.term == resource_type_identifier
258
+ }.first.type_identifier
259
+
260
+ location = @model.get_by_id(type_identifier).location
261
+ resource_type_identifier = @endpoint + location
262
+ end
263
+
264
+ raise "Unknown resource identifier! #{resource_type_identifier}" unless resource_type_identifier.start_with? @endpoint
265
+
266
+ # encapsulate the acion in a collection
267
+ collection = Occi::Collection.new
268
+ scheme, term = action.split(' #')
269
+ collection.actions << Occi::Core::Action.new(scheme + '#', term)
270
+
271
+ # make the request
272
+ path = sanitize_resource_link(resource_type_identifier) + '?action=' + term
273
+ post path, collection
274
+ end
275
+
276
+ # @see Occi::Api::Client::ClientBase
277
+ def refresh
278
+ # re-download the model from the server
279
+ model = get('/-/')
280
+ set_model model
281
+ end
282
+
283
+ private
284
+
285
+ # Performs GET request and parses the responses to collections.
286
+ #
287
+ # @example
288
+ # get "/-/" # => #<Occi::Collection>
289
+ # get "/compute/" # => #<Occi::Collection>
290
+ # get "/compute/fs65g4fs6g-sf54g54gsf-aa12faddf52" # => #<Occi::Collection>
291
+ #
292
+ # @param [String] path for the GET request
293
+ # @param [Occi::Collection] collection of filters
294
+ # @return [Occi::Collection] parsed result of the request
295
+ def get(path='', filter=nil)
296
+ # remove the leading slash
297
+ path = path.gsub(/\A\//, '')
298
+
299
+ response = if filter
300
+ categories = filter.categories.collect { |category| category.to_text }.join(',')
301
+ attributes = filter.entities.collect { |entity| entity.attributes.combine.collect { |k, v| k + '=' + v } }.join(',')
302
+
303
+ headers = self.class.headers.clone
304
+ headers['Content-Type'] = 'text/occi'
305
+ headers['Category'] = categories unless categories.empty?
306
+ headers['X-OCCI-Attributes'] = attributes unless attributes.empty?
307
+
308
+ self.class.get(@endpoint + path, :headers => headers)
309
+ else
310
+ self.class.get(@endpoint + path)
311
+ end
312
+
313
+ response_msg = response_message response
314
+ raise "HTTP GET failed! #{response_msg}" unless response.code.between? 200, 300
315
+
316
+ Occi::Log.debug "Response location: #{('/' + path).match(/\/.*\//).to_s}"
317
+ kind = @model.get_by_location(('/' + path).match(/\/.*\//).to_s) if @model
318
+
319
+ Occi::Log.debug "Response kind: #{kind}"
320
+
321
+ if kind
322
+ kind.related_to? Occi::Core::Resource ? entity_type = Occi::Core::Resource : entity_type = nil
323
+ entity_type = Occi::Core::Link if kind.related_to? Occi::Core::Link
324
+ end
325
+
326
+ Occi::Log.debug "Parser call: #{response.content_type} #{entity_type} #{path.include?('-/')}"
327
+ collection = Occi::Parser.parse(response.content_type, response.body, path.include?('-/'), entity_type, response.headers)
328
+
329
+ Occi::Log.debug "Parsed collection: empty? #{collection.empty?}"
330
+ collection
331
+ end
332
+
333
+ # Performs POST requests and returns URI locations. Resource data must be provided
334
+ # in an Occi::Collection instance.
335
+ #
336
+ # @example
337
+ # collection = Occi::Collection.new
338
+ # collection.resources << entity if entity.kind_of? Occi::Core::Resource
339
+ # collection.links << entity if entity.kind_of? Occi::Core::Link
340
+ #
341
+ # post "/compute/", collection # => "http://localhost:3300/compute/23sf4g65as-asdgsg2-sdfgsf2g"
342
+ # post "/network/", collection # => "http://localhost:3300/network/23sf4g65as-asdgsg2-sdfgsf2g"
343
+ # post "/storage/", collection # => "http://localhost:3300/storage/23sf4g65as-asdgsg2-sdfgsf2g"
344
+ #
345
+ # @param [String] path for the POST request
346
+ # @param [Occi::Collection] resource data to be POSTed
347
+ # @return [String] URI location
348
+ def post(path, collection)
349
+ # remove the leading slash
350
+ path = path.gsub(/\A\//, '')
351
+
352
+ headers = self.class.headers.clone
353
+ headers['Content-Type'] = @media_type
354
+
355
+ response = case @media_type
356
+ when 'application/occi+json'
357
+ self.class.post(@endpoint + path,
358
+ :body => collection.to_json,
359
+ :headers => headers)
360
+ when 'text/occi'
361
+ self.class.post(@endpoint + path,
362
+ :headers => collection.to_header.merge(headers))
363
+ else
364
+ self.class.post(@endpoint + path,
365
+ :body => collection.to_text,
366
+ :headers => headers)
367
+ end
368
+
369
+ response_msg = response_message response
370
+
371
+ case response.code
372
+ when 200
373
+ collection = Occi::Parser.parse(response.header["content-type"].split(";").first, response.body)
374
+
375
+ if collection.empty?
376
+ Occi::Parser.locations(response.header["content-type"].split(";").first, response.body, response.headers).first
377
+ else
378
+ collection.resources.first.location if collection.resources.first
379
+ end
380
+ when 201
381
+ Occi::Parser.locations(response.header["content-type"].split(";").first, response.body, response.headers).first
382
+ else
383
+ raise "HTTP POST failed! #{response_msg}"
384
+ end
385
+ end
386
+
387
+ # Performs PUT requests and parses responses to collections.
388
+ #
389
+ # @example
390
+ # TODO: add examples
391
+ #
392
+ # @param [String] path for the PUT request
393
+ # @param [Occi::Collection] resource data to send
394
+ # @return [Occi::Collection] parsed result of the request
395
+ def put(path, collection)
396
+ # remove the leading slash
397
+ path = path.gsub(/\A\//, '')
398
+
399
+ headers = self.class.headers.clone
400
+ headers['Content-Type'] = @media_type
401
+
402
+ response = case @media_type
403
+ when 'application/occi+json'
404
+ self.class.post(@endpoint + path,
405
+ :body => collection.to_json,
406
+ :headers => headers)
407
+ when 'text/occi'
408
+ self.class.post(@endpoint + path,
409
+ :headers => collection.to_header.merge(headers))
410
+ else
411
+ self.class.post(@endpoint + path,
412
+ :body => collection.to_text,
413
+ :headers => headers)
414
+ end
415
+
416
+ response_msg = response_message response
417
+
418
+ case response.code
419
+ when 200, 201
420
+ Occi::Parser.parse(response.header["content-type"].split(";").first, response.body)
421
+ else
422
+ raise "HTTP POST failed! #{response_msg}"
423
+ end
424
+ end
425
+
426
+ # Performs DELETE requests and returns True on success.
427
+ #
428
+ # @example
429
+ # del "/compute/65sf4g65sf4g-sf6g54sf5g-sfgsf32g3" # => true
430
+ #
431
+ # @param [String] path for the DELETE request
432
+ # @param [Occi::Collection] collection of filters (currently NOT used)
433
+ # @return [Boolean] status
434
+ def del(path, filter=nil)
435
+ # remove the leading slash
436
+ path = path.gsub(/\A\//, '')
437
+
438
+ response = self.class.delete(@endpoint + path)
439
+
440
+ response_msg = response_message response
441
+ raise "HTTP DELETE failed! #{response_msg}" unless response.code.between? 200, 300
442
+
443
+ true
444
+ end
445
+
446
+ # @see Occi::Api::Client::ClientBase
447
+ def set_logger(log_options)
448
+ super log_options
449
+
450
+ self.class.debug_output $stderr if log_options[:level] == Occi::Log::DEBUG
451
+ end
452
+
453
+ # @see Occi::Api::Client::ClientBase
454
+ def set_auth(auth_options, fallback = false)
455
+ @auth_options = auth_options
456
+
457
+ case @auth_options[:type]
458
+ when "basic"
459
+ @authn_plugin = Http::AuthnPlugins::Basic.new self, @auth_options
460
+ when "digest"
461
+ @authn_plugin = Http::AuthnPlugins::Digest.new self, @auth_options
462
+ when "x509"
463
+ @authn_plugin = Http::AuthnPlugins::X509.new self, @auth_options
464
+ when "keystone"
465
+ raise ::Occi::Api::Client::Errors::AuthnError, "This authN method is for fallback only!" unless fallback
466
+ @authn_plugin = Http::AuthnPlugins::Keystone.new self, @auth_options
467
+ when "none", nil
468
+ @authn_plugin = Http::AuthnPlugins::Dummy.new self
469
+ else
470
+ raise ::Occi::Api::Client::Errors::AuthnError, "Unknown authN method [#{@auth_options[:type]}]!"
471
+ end
472
+
473
+ @authn_plugin.setup
474
+ end
475
+
476
+ # @see Occi::Api::Client::ClientBase
477
+ def preauthenticate
478
+ begin
479
+ @authn_plugin.authenticate
480
+ rescue ::Occi::Api::Client::Errors::AuthnError => e
481
+ Occi::Log.debug e.message
482
+
483
+ if @authn_plugin.fallbacks.any?
484
+ @auth_options[:original_type] = @auth_options[:type]
485
+ @auth_options[:type] = @authn_plugin.fallbacks.first
486
+
487
+ set_auth @auth_options, true
488
+ @authn_plugin.authenticate
489
+ else
490
+ raise e
491
+ end
492
+ end
493
+ end
494
+
495
+ # @see Occi::Api::Client::ClientBase
496
+ def set_media_type(force_type = nil)
497
+ # force media_type if provided
498
+ if force_type
499
+ self.class.headers 'Accept' => force_type
500
+ @media_type = force_type
501
+ else
502
+ media_types = self.class.head(@endpoint).headers['accept']
503
+ Occi::Log.debug("Available media types: #{media_types}")
504
+ @media_type = case media_types
505
+ when /application\/occi\+json/
506
+ 'application/occi+json'
507
+ else
508
+ 'text/plain'
509
+ end
510
+ end
511
+ end
512
+
513
+ # Generates a human-readable response message based on the HTTP response code.
514
+ #
515
+ # @example
516
+ # response_message self.class.delete(@endpoint + path)
517
+ # # => 'HTTP Response status: [200] OK'
518
+ #
519
+ # @param [HTTParty::Response] HTTParty response object
520
+ # @return [String] message
521
+ def response_message(response)
522
+ @last_response = response
523
+ 'HTTP Response status: [' + response.code.to_s + '] ' + reason_phrase(response.code)
524
+ end
525
+
526
+ # Converts HTTP response codes to human-readable phrases.
527
+ #
528
+ # @example
529
+ # reason_phrase(500) # => "Internal Server Error"
530
+ #
531
+ # @param [Integer] HTTP response code
532
+ # @return [String] human-readable phrase
533
+ def reason_phrase(code)
534
+ HTTP_CODES[code.to_s]
535
+ end
536
+
537
+ end
538
+
539
+ end
540
+ end
541
+ end
@@ -0,0 +1,7 @@
1
+ module Occi::Api::Client
2
+ module Errors
3
+
4
+ class AuthnError < RuntimeError; end
5
+
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ require 'occi/api/client/errors/authn_error'
@@ -0,0 +1,27 @@
1
+ module Occi::Api::Client
2
+ module Http
3
+ module AuthnPlugins
4
+
5
+ class Base
6
+ attr_reader :env_ref
7
+ attr_reader :options
8
+ attr_reader :fallbacks
9
+
10
+ def initialize(env_ref, options = {})
11
+ @options = options
12
+ @env_ref = env_ref
13
+ @fallbacks = []
14
+ end
15
+
16
+ def setup(options = {}); end
17
+
18
+ def authenticate(options = {})
19
+ response = @env_ref.class.head @env_ref.endpoint
20
+ raise ::Occi::Api::Client::Errors::AuthnError, "Authentication failed with code #{response.code.to_s}!" unless response.success?
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ module Occi::Api::Client
2
+ module Http
3
+ module AuthnPlugins
4
+
5
+ class Basic < Base
6
+
7
+ def initialize(env_ref, options = {})
8
+ super env_ref, options
9
+ @fallbacks = %w(keystone)
10
+ end
11
+
12
+ def setup(options = {})
13
+ # set up basic auth
14
+ raise ArgumentError, "Missing required options 'username' and 'password' for basic auth!" unless @options[:username] && @options[:password]
15
+ @env_ref.class.basic_auth @options[:username], @options[:password]
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module Occi::Api::Client
2
+ module Http
3
+ module AuthnPlugins
4
+
5
+ class Digest < Base
6
+
7
+ def initialize(env_ref, options = {})
8
+ super env_ref, options
9
+ @fallbacks = %w(keystone)
10
+ end
11
+
12
+ def setup(options = {})
13
+ # set up digest auth
14
+ raise ArgumentError, "Missing required options 'username' and 'password' for digest auth!" unless @options[:username] and @options[:password]
15
+ @env_ref.class.digest_auth @options[:username], @options[:password]
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ module Occi::Api::Client
2
+ module Http
3
+ module AuthnPlugins
4
+
5
+ class Dummy < Base
6
+
7
+ def authenticate(options = {}); end
8
+
9
+ end
10
+
11
+ end
12
+ end
13
+ end