occi 2.5.19 → 3.0.0.beta.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/.gitignore +4 -0
  2. data/.travis.yml +3 -1
  3. data/AUTHORS +4 -3
  4. data/Gemfile +9 -0
  5. data/Gemfile.lock +64 -21
  6. data/README.md +19 -14
  7. data/Rakefile +12 -8
  8. data/Test Results - discovery_interface.html +862 -0
  9. data/bin/occi +333 -83
  10. data/examples/dsl_example.rb +6 -6
  11. data/examples/x509auth_example.rb +6 -6
  12. data/features/common/step_definitions/common_steps.rb +32 -0
  13. data/features/occi/core/create/create.feature +17 -0
  14. data/features/occi/core/create/step_definitions/create_steps.rb +0 -0
  15. data/features/occi/core/delete/delete.feature +14 -0
  16. data/features/occi/core/delete/step_definitions/delete_steps.rb +0 -0
  17. data/features/occi/core/discovery_interface/discovery_interface.feature +35 -0
  18. data/features/occi/core/discovery_interface/step_definitions/discovery_interface_steps.rb +19 -0
  19. data/features/occi/core/miscellaneous/miscellaneous.feature +14 -0
  20. data/features/occi/core/miscellaneous/step_definitions/miscellaneous_steps.rb +0 -0
  21. data/features/occi/core/read/read.feature +14 -0
  22. data/features/occi/core/read/step_definitions/read_steps.rb +0 -0
  23. data/features/occi/core/update/step_definitions/update_steps.rb +0 -0
  24. data/features/occi/core/update/update.feature +14 -0
  25. data/features/occi/infrastructure/create/create.feature +14 -0
  26. data/features/occi/infrastructure/create/step_definitions/create_steps.rb +0 -0
  27. data/features/support/env.rb +4 -0
  28. data/lib/occi.rb +29 -3
  29. data/lib/occi/api/client/client_amqp.rb +756 -0
  30. data/lib/occi/api/client/client_http.rb +922 -0
  31. data/lib/occi/api/client/http/httparty_fix.rb +53 -0
  32. data/lib/occi/api/client/http/net_http_fix.rb +46 -0
  33. data/lib/occi/api/dsl.rb +77 -73
  34. data/lib/occi/bin/helpers.rb +91 -0
  35. data/lib/occi/bin/occi_opts.rb +251 -0
  36. data/lib/occi/bin/resource_output_factory.rb +90 -0
  37. data/lib/occi/bin/templates/compute.erb +15 -0
  38. data/lib/occi/bin/templates/network.erb +11 -0
  39. data/lib/occi/bin/templates/os_tpl.erb +9 -0
  40. data/lib/occi/bin/templates/resource_tpl.erb +9 -0
  41. data/lib/occi/bin/templates/storage.erb +10 -0
  42. data/lib/occi/collection.rb +122 -25
  43. data/lib/occi/core.rb +18 -9
  44. data/lib/occi/core/action.rb +20 -4
  45. data/lib/occi/core/action_instance.rb +24 -0
  46. data/lib/occi/core/actions.rb +22 -0
  47. data/lib/occi/core/attribute_properties.rb +33 -84
  48. data/lib/occi/core/attributes.rb +32 -14
  49. data/lib/occi/core/categories.rb +46 -0
  50. data/lib/occi/core/category.rb +94 -20
  51. data/lib/occi/core/entities.rb +50 -0
  52. data/lib/occi/core/entity.rb +130 -89
  53. data/lib/occi/core/kind.rb +28 -35
  54. data/lib/occi/core/kinds.rb +22 -0
  55. data/lib/occi/core/link.rb +43 -40
  56. data/lib/occi/core/links.rb +34 -0
  57. data/lib/occi/core/mixin.rb +28 -23
  58. data/lib/occi/core/mixins.rb +22 -0
  59. data/lib/occi/core/related.rb +20 -0
  60. data/lib/occi/core/resource.rb +40 -40
  61. data/lib/occi/core/resources.rb +14 -0
  62. data/lib/occi/infrastructure.rb +27 -0
  63. data/lib/occi/infrastructure/compute.rb +159 -0
  64. data/lib/occi/infrastructure/network.rb +131 -0
  65. data/lib/occi/infrastructure/network/ipnetwork.rb +34 -0
  66. data/lib/occi/infrastructure/networkinterface.rb +124 -0
  67. data/lib/occi/infrastructure/networkinterface/ipnetworkinterface.rb +34 -0
  68. data/lib/occi/infrastructure/os_tpl.rb +19 -0
  69. data/lib/occi/infrastructure/resource_tpl.rb +19 -0
  70. data/lib/occi/infrastructure/storage.rb +96 -0
  71. data/lib/occi/infrastructure/storagelink.rb +73 -0
  72. data/lib/occi/log.rb +6 -2
  73. data/lib/occi/model.rb +38 -70
  74. data/lib/occi/parser.rb +108 -88
  75. data/lib/occi/version.rb +2 -2
  76. data/lib/occiantlr/OCCIANTLR.g +6 -5
  77. data/lib/occiantlr/OCCIANTLRLexer.rb +52 -52
  78. data/lib/occiantlr/OCCIANTLRParser.rb +678 -569
  79. data/lib/occiantlr/README.md +1 -1
  80. data/occi.gemspec +2 -1
  81. data/spec/cassettes/client_http_text_plain.yml +1066 -0
  82. data/spec/occi/api/client/client_amqp_spec.rb +148 -0
  83. data/spec/occi/api/client/client_http_0.5_spec.rb +292 -0
  84. data/spec/occi/api/client/client_http_spec.rb +259 -0
  85. data/spec/occi/api/dsl_spec.rb +0 -0
  86. data/spec/occi/collection_spec.rb +23 -10
  87. data/spec/occi/core/categories_spec.rb +30 -0
  88. data/spec/occi/core/category_spec.rb +41 -0
  89. data/spec/occi/core/entity_spec.rb +52 -0
  90. data/spec/occi/core/resource_spec.rb +21 -0
  91. data/spec/occi/infrastructure/compute_spec.rb +32 -0
  92. data/spec/occi/log_spec.rb +10 -10
  93. data/spec/occi/model_spec.rb +24 -24
  94. data/spec/occi/parser_spec.rb +89 -39
  95. data/spec/occi/test.json +22 -58
  96. data/spec/occiantlr/parser_spec.rb +5 -7
  97. data/spec/spec_helper.rb +13 -3
  98. metadata +116 -19
  99. data/etc/model/infrastructure/compute.json +0 -108
  100. data/etc/model/infrastructure/ipnetwork.json +0 -40
  101. data/etc/model/infrastructure/ipnetworkinterface.json +0 -40
  102. data/etc/model/infrastructure/network.json +0 -55
  103. data/etc/model/infrastructure/networkinterface.json +0 -38
  104. data/etc/model/infrastructure/os_template.json +0 -9
  105. data/etc/model/infrastructure/resource_template.json +0 -9
  106. data/etc/model/infrastructure/storage.json +0 -72
  107. data/etc/model/infrastructure/storagelink.json +0 -38
  108. data/lib/occi/api/client.rb +0 -596
  109. data/lib/occi/client/occiopts.rb +0 -146
  110. data/spec/occi/client_spec.rb +0 -12
@@ -0,0 +1,922 @@
1
+ require 'rubygems'
2
+ require 'httparty'
3
+ require 'occi/api/client/http/net_http_fix'
4
+ require 'occi/api/client/http/httparty_fix'
5
+
6
+ module Occi
7
+ module Api
8
+ module Client
9
+
10
+ class ClientHttp
11
+
12
+ # HTTParty for raw HTTP requests
13
+ include HTTParty
14
+
15
+ # TODO: uncomment the following line as JSON is properly implemented in OpenStack
16
+ # headers 'Accept' => 'application/occi+json,text/plain;q=0.8,text/occi;q=0.2'
17
+ headers 'Accept' => 'text/plain,text/occi;q=0.2'
18
+
19
+ # a few attributes which should be visible outside the client
20
+ attr_reader :endpoint
21
+ attr_reader :auth_options
22
+ attr_accessor :media_type
23
+ attr_reader :connected
24
+ attr_accessor :model
25
+ attr_reader :logger
26
+ attr_reader :last_response
27
+
28
+ # hash mapping HTTP response codes to human-readable messages
29
+ HTTP_CODES = {
30
+ "100" => "Continue",
31
+ "101" => "Switching Protocols",
32
+ "200" => "OK",
33
+ "201" => "Created",
34
+ "202" => "Accepted",
35
+ "203" => "Non-Authoritative Information",
36
+ "204" => "No Content",
37
+ "205" => "Reset Content",
38
+ "206" => "Partial Content",
39
+ "300" => "Multiple Choices",
40
+ "301" => "Moved Permanently",
41
+ "302" => "Found",
42
+ "303" => "See Other",
43
+ "304" => "Not Modified",
44
+ "305" => "Use Proxy",
45
+ "307" => "Temporary Redirect",
46
+ "400" => "Bad Request",
47
+ "401" => "Unauthorized",
48
+ "402" => "Payment Required",
49
+ "403" => "Forbidden",
50
+ "404" => "Not Found",
51
+ "405" => "Method Not Allowed",
52
+ "406" => "Not Acceptable",
53
+ "407" => "Proxy Authentication Required",
54
+ "408" => "Request Time-out",
55
+ "409" => "Conflict",
56
+ "410" => "Gone",
57
+ "411" => "Length Required",
58
+ "412" => "Precondition Failed",
59
+ "413" => "Request Entity Too Large",
60
+ "414" => "Request-URI Too Large",
61
+ "415" => "Unsupported Media Type",
62
+ "416" => "Requested range not satisfiable",
63
+ "417" => "Expectation Failed",
64
+ "500" => "Internal Server Error",
65
+ "501" => "Not Implemented",
66
+ "502" => "Bad Gateway",
67
+ "503" => "Service Unavailable",
68
+ "504" => "Gateway Time-out",
69
+ "505" => "HTTP Version not supported"
70
+ }
71
+
72
+ # Initializes client data structures and retrieves OCCI model
73
+ # from the server.
74
+ #
75
+ # @example
76
+ # Occi::Api::Client::ClientHttp.new # => #<Occi::Api::Client::ClientHttp>
77
+ #
78
+ # @param [String] endpoint URI
79
+ # @param [Hash] auth options in a hash
80
+ # @param [Hash] logging options in a hash
81
+ # @param [Boolean] enable autoconnect
82
+ # @param [String] media type identifier
83
+ # @return [Occi::Api::Client::ClientHttp] client instance
84
+ def initialize(endpoint = "http://localhost:3000/", auth_options = { :type => "none" },
85
+ log_options = { :out => STDERR, :level => Occi::Log::WARN, :logger => nil },
86
+ auto_connect = true, media_type = nil)
87
+ # set Occi::Log
88
+ set_logger log_options
89
+
90
+ # pass auth options to HTTParty
91
+ change_auth auth_options
92
+
93
+ # check the validity and canonize the endpoint URI
94
+ prepare_endpoint endpoint
95
+
96
+ # get accepted media types from HTTParty
97
+ set_media_type
98
+
99
+ # force media_type if provided
100
+ if media_type
101
+ self.class.headers 'Accept' => media_type
102
+ @media_type = media_type
103
+ end
104
+
105
+ Occi::Log.debug("Media Type: #{@media_type}")
106
+ Occi::Log.debug("Headers: #{self.class.headers}")
107
+
108
+ # get model information from the endpoint
109
+ # and create Occi::Model instance
110
+ set_model
111
+
112
+ # auto-connect?
113
+ @connected = auto_connect
114
+ end
115
+
116
+ # Creates a new resource instance, resource should be specified
117
+ # by its name or identifier.
118
+ #
119
+ # @example
120
+ # client.get_resource "compute" # => Occi::Core::Resource
121
+ # client.get_resource "storage" # => Occi::Core::Resource
122
+ # client.get_resource "http://schemas.ogf.org/occi/infrastructure#network"
123
+ # # => Occi::Core::Resource
124
+ #
125
+ # @param [String] resource name or resource identifier
126
+ # @return [Occi::Core::Resource] new resource instance
127
+ def get_resource(resource_type)
128
+
129
+ Occi::Log.debug("Instantiating #{resource_type} ...")
130
+
131
+ if @model.get_by_id resource_type
132
+ # we got a resource type identifier
133
+ Occi::Core::Resource.new resource_type
134
+ elsif @model.kinds.select { |kind| kind.term == resource_type }.any?
135
+ # we got a resource type name
136
+ Occi::Core::Resource.new @model.kinds.select {
137
+ |kind| kind.term == resource_type
138
+ }.first.type_identifier
139
+ else
140
+ raise "Unknown resource type! [#{resource_type}]"
141
+ end
142
+
143
+ end
144
+
145
+ # Retrieves all available resource types.
146
+ #
147
+ # @example
148
+ # client.get_resource_types # => [ "compute", "storage", "network" ]
149
+ #
150
+ # @return [Array<String>] list of available resource types in a human-readable format
151
+ def get_resource_types
152
+ Occi::Log.debug("Getting resource types ...")
153
+ @model.kinds.collect { |kind| kind.term }
154
+ end
155
+
156
+ # Retrieves all available resource type identifiers.
157
+ #
158
+ # @example
159
+ # client.get_resource_type_identifiers
160
+ # # => [ "http://schemas.ogf.org/occi/infrastructure#compute",
161
+ # # "http://schemas.ogf.org/occi/infrastructure#storage",
162
+ # # "http://schemas.ogf.org/occi/infrastructure#network" ]
163
+ #
164
+ # @return [Array<String>] list of available resource types in a Occi ID format
165
+ def get_resource_type_identifiers
166
+ Occi::Log.debug("Getting resource identifiers ...")
167
+ @model.kinds.collect { |kind| kind.type_identifier }
168
+ end
169
+
170
+ # Looks up a mixin using its name and, optionally, a type as well.
171
+ # Will return mixin's full location (a link) or a description.
172
+ #
173
+ # @example
174
+ # client.find_mixin "debian6"
175
+ # # => "http://my.occi.service/occi/infrastructure/os_tpl#debian6"
176
+ # client.find_mixin "debian6", "os_tpl"
177
+ # # => "http://my.occi.service/occi/infrastructure/os_tpl#debian6"
178
+ # client.find_mixin "large", "resource_tpl"
179
+ # # => "http://my.occi.service/occi/infrastructure/resource_tpl#large"
180
+ # client.find_mixin "debian6", "resource_tpl" # => nil
181
+ #
182
+ # @param [String] name of the mixin
183
+ # @param [String] type of the mixin
184
+ # @param [Boolean] should we describe the mixin or return its link?
185
+ # @return [String, Occi::Collection, nil] link, mixin description or nothing found
186
+ def find_mixin(name, type = nil, describe = false)
187
+
188
+ Occi::Log.debug("Looking for mixin #{name} + #{type} + #{describe}")
189
+
190
+ # is type valid?
191
+ if type
192
+ raise "Unknown mixin type! [#{type}]" unless @mixins.has_key? type.to_sym
193
+ end
194
+
195
+ # TODO: extend this code to support multiple matches and regex filters
196
+ # should we look for links or descriptions?
197
+ if describe
198
+ # we are looking for descriptions
199
+ if type
200
+ # get the first match from either os_tpls or resource_tpls
201
+ case
202
+ when type == "os_tpl"
203
+ get_os_templates.select { |mixin| mixin.term == name }.first
204
+ when type == "resource_tpl"
205
+ get_resource_templates.select { |template| template.term == name }.first
206
+ else
207
+ nil
208
+ end
209
+ else
210
+ # try in os_tpls first
211
+ found = get_os_templates.select { |os| os.term == name }.first
212
+
213
+ # then try in resource_tpls
214
+ found = get_resource_templates.select {
215
+ |template| template.term == name
216
+ }.first unless found
217
+
218
+ found
219
+ end
220
+ else
221
+ # we are looking for links
222
+ # prefix mixin name with '#' to simplify the search
223
+ name = "#" + name
224
+ if type
225
+ # return the first match with the selected type
226
+ @mixins[type.to_sym].select {
227
+ |mixin| mixin.to_s.reverse.start_with? name.reverse
228
+ }.first
229
+ else
230
+ # there is no type preference, return first global match
231
+ @mixins.flatten(2).select {
232
+ |mixin| mixin.to_s.reverse.start_with? name.reverse
233
+ }.first
234
+ end
235
+ end
236
+ end
237
+
238
+ # Retrieves available mixins of a specified type or all available
239
+ # mixins if the type wasn't specified. Mixins are returned in the
240
+ # form of mixin identifiers.
241
+ #
242
+ # @example
243
+ # client.get_mixins
244
+ # # => ["http://my.occi.service/occi/infrastructure/os_tpl#debian6",
245
+ # # "http://my.occi.service/occi/infrastructure/resource_tpl#small"]
246
+ # client.get_mixins "os_tpl"
247
+ # # => ["http://my.occi.service/occi/infrastructure/os_tpl#debian6"]
248
+ # client.get_mixins "resource_tpl"
249
+ # # => ["http://my.occi.service/occi/infrastructure/resource_tpl#small"]
250
+ #
251
+ # @param [String] type of mixins
252
+ # @return [Array<String>] list of available mixins
253
+ def get_mixins(type = nil)
254
+ if type
255
+ # is type valid?
256
+ raise "Unknown mixin type! #{type}" unless @mixins.has_key? type.to_sym
257
+
258
+ # return mixin of the selected type
259
+ @mixins[type.to_sym]
260
+ else
261
+ # we did not get a type, return all mixins
262
+ mixins = []
263
+
264
+ # flatten the hash and remove its keys
265
+ get_mixin_types.each do |ltype|
266
+ mixins.concat @mixins[ltype.to_sym]
267
+ end
268
+
269
+ mixins
270
+ end
271
+ end
272
+
273
+ # Retrieves available mixin types. Mixin types are presented
274
+ # in a shortened format (i.e. not as type identifiers).
275
+ #
276
+ # @example
277
+ # client.get_mixin_types # => [ "os_tpl", "resource_tpl" ]
278
+ #
279
+ # @return [Array<String>] list of available mixin types
280
+ def get_mixin_types
281
+ @mixins.keys.map! { |k| k.to_s }
282
+ end
283
+
284
+ # Retrieves available mixin type identifiers.
285
+ #
286
+ # @example
287
+ # client.get_mixin_type_identifiers
288
+ # # => ['http://schemas.ogf.org/occi/infrastructure#os_tpl',
289
+ # # 'http://schemas.ogf.org/occi/infrastructure#resource_tpl']
290
+ #
291
+ # @return [Array<String>] list of available mixin type identifiers
292
+ def get_mixin_type_identifiers
293
+ identifiers = []
294
+
295
+ get_mixin_types.each do |mixin_type|
296
+ identifiers << 'http://schemas.ogf.org/occi/infrastructure#' + mixin_type
297
+ end
298
+
299
+ identifiers
300
+ end
301
+
302
+ # Retrieves available resources represented by resource locations (URIs).
303
+ # If no type identifier is specified, all available resource are listed.
304
+ # Type identifier can be specified in its shortened format (e.g. "compute",
305
+ # "storage", "network").
306
+ #
307
+ # @example
308
+ # client.list
309
+ # # => [ "http://localhost:3300/compute/jh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j",
310
+ # # "http://localhost:3300/network/kh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j",
311
+ # # "http://localhost:3300/storage/lh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j" ]
312
+ # client.list "compute"
313
+ # # => [ "http://localhost:3300/compute/jh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j" ]
314
+ # client.list "http://schemas.ogf.org/occi/infrastructure#compute"
315
+ # # => [ "http://localhost:3300/compute/jh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j" ]
316
+ #
317
+ # @param [String] resource type identifier or just type name
318
+ # @return [Array<String>] list of links
319
+ def list(resource_type_identifier=nil)
320
+ if resource_type_identifier
321
+ # convert type to type identifier
322
+ resource_type_identifier = @model.kinds.select {
323
+ |kind| kind.term == resource_type_identifier
324
+ }.first.type_identifier if @model.kinds.select {
325
+ |kind| kind.term == resource_type_identifier
326
+ }.any?
327
+
328
+ # check some basic pre-conditions
329
+ raise "Endpoint is not connected!" unless @connected
330
+ raise "Unkown resource type identifier! [#{resource_type_identifier}]" unless @model.get_by_id resource_type_identifier
331
+
332
+ # split the type identifier and get the most important part
333
+ uri_part = resource_type_identifier.split('#').last
334
+
335
+ # request uri-list from the server
336
+ path = uri_part + '/'
337
+ else
338
+ path = '/'
339
+ end
340
+
341
+ self.class.get(@endpoint + path,
342
+ :headers => { "Accept" => 'text/uri-list' }).body.split("\n").compact
343
+ end
344
+
345
+ # Retrieves descriptions for available resources specified by a type
346
+ # identifier or resource location. If no type identifier or location
347
+ # is specified, all available resources in all available resource types
348
+ # will be described.
349
+ #
350
+ # @example
351
+ # client.describe
352
+ # # => [#<Occi::Collection>, #<Occi::Collection>, #<Occi::Collection>]
353
+ # client.describe "compute"
354
+ # # => [#<Occi::Collection>, #<Occi::Collection>, #<Occi::Collection>]
355
+ # client.describe "http://schemas.ogf.org/occi/infrastructure#compute"
356
+ # # => [#<Occi::Collection>, #<Occi::Collection>, #<Occi::Collection>]
357
+ # client.describe "http://localhost:3300/compute/j5hk1234jk2524-2j3j2k34jjh234-adfaf1234"
358
+ # # => [#<Occi::Collection>]
359
+ #
360
+ # @param [String] resource type identifier, type name or resource location
361
+ # @return [Array<Occi::Collection>] list of resource descriptions
362
+ def describe(resource_type_identifier=nil)
363
+
364
+ # convert type to type identifier
365
+ resource_type_identifier = @model.kinds.select {
366
+ |kind| kind.term == resource_type_identifier
367
+ }.first.type_identifier if @model.kinds.select {
368
+ |kind| kind.term == resource_type_identifier
369
+ }.any?
370
+
371
+ # check some basic pre-conditions
372
+ raise "Endpoint is not connected!" unless @connected
373
+
374
+ descriptions = []
375
+
376
+ if resource_type_identifier.nil?
377
+ descriptions << get('/')
378
+ elsif @model.get_by_id resource_type_identifier
379
+ # we got type identifier
380
+ # get all available resources of this type
381
+ locations = list resource_type_identifier
382
+
383
+ # make the requests
384
+ locations.each do |location|
385
+ descriptions << get(sanitize_resource_link(location))
386
+ end
387
+ elsif resource_type_identifier.start_with? @endpoint
388
+ # we got resource link
389
+ # make the request
390
+ descriptions << get(sanitize_resource_link(resource_type_identifier))
391
+ else
392
+ raise "Unkown resource type identifier! [#{resource_type_identifier}]"
393
+ end
394
+
395
+ descriptions
396
+ end
397
+
398
+ # Creates a new resource on the server. Resource must be provided
399
+ # as an instance of Occi::Core::Entity, e.g. instantiated using
400
+ # the get_resource method.
401
+ #
402
+ # @example
403
+ # res = client.get_resource "compute"
404
+ #
405
+ # res.title = "MyComputeResource1"
406
+ # res.mixins << client.find_mixin('small', "resource_tpl")
407
+ # res.mixins << client.find_mixin('debian6', "os_tpl")
408
+ #
409
+ # client.create res # => "http://localhost:3300/compute/df7698...f987fa"
410
+ #
411
+ # @param [Occi::Core::Entity] resource to be created on the server
412
+ # @return [String] URI of the new resource
413
+ def create(entity)
414
+
415
+ # check some basic pre-conditions
416
+ raise "Endpoint is not connected!" unless @connected
417
+ raise "#{entity} not an entity" unless entity.kind_of? Occi::Core::Entity
418
+
419
+ # is this entity valid?
420
+ entity.model = @model
421
+ entity.check
422
+
423
+ Occi::Log.debug "Entity kind: #{entity.kind}"
424
+ kind = entity.kind
425
+ raise "No kind found for #{entity}" unless kind
426
+
427
+ # get location for this kind of entity
428
+ Occi::Log.debug "Kind location: #{entity.kind.location}"
429
+ location = kind.location
430
+ collection = Occi::Collection.new
431
+
432
+ # is this entity a Resource or a Link?
433
+ Occi::Log.debug "Entity class: #{entity.class.name}"
434
+ collection.resources << entity if entity.kind_of? Occi::Core::Resource
435
+ collection.links << entity if entity.kind_of? Occi::Core::Link
436
+
437
+ # make the request
438
+ post location, collection
439
+ end
440
+
441
+ # Deploys a compute resource based on an OVF/OVA descriptor available
442
+ # on a local file system.
443
+ #
444
+ # @example
445
+ # client.deploy "~/MyVMs/rOcciVM.ovf" # => "http://localhost:3300/compute/343423...42njhdafa"
446
+ #
447
+ # @param [String] location of an OVF/OVA file
448
+ # @return [String] URI of the new resource
449
+ def deploy(location)
450
+ media_types = self.class.head(@endpoint).headers['accept'].to_s
451
+ raise "File #{location} does not exist" unless File.exist? location
452
+
453
+ file = File.read(location)
454
+
455
+ if location.include? '.ovf'
456
+ if media_types.include? 'application/ovf'
457
+ headers = self.class.headers.clone
458
+ headers['Content-Type'] = 'application/ovf'
459
+ self.class.post(@endpoint + '/compute/',
460
+ :body => file,
461
+ :headers => headers)
462
+ end
463
+ elsif location.include? '.ova'
464
+ if media_types.include? ' application/ova '
465
+ headers = self.class.headers.clone
466
+ headers['Content-Type'] = 'application/ova'
467
+ self.class.post(@endpoint + '/compute/',
468
+ :body => file,
469
+ :headers => headers)
470
+ end
471
+ end
472
+ end
473
+
474
+ # Deletes a resource or all resource of a certain resource type
475
+ # from the server.
476
+ #
477
+ # @example
478
+ # client.delete "compute" # => true
479
+ # client.delete "http://schemas.ogf.org/occi/infrastructure#compute" # => true
480
+ # client.delete "http://localhost:3300/compute/245j42594...98s9df8s9f" # => true
481
+ #
482
+ # @param [String] resource type identifier, type name or location
483
+ # @return [Boolean] status
484
+ def delete(resource_type_identifier)
485
+ # convert type to type identifier
486
+ raise "Endpoint is not connected!" unless @connected
487
+
488
+ path = path_for_resource_type resource_type_identifier
489
+
490
+ del path
491
+ end
492
+
493
+ # Triggers given action on a specific resource.
494
+ #
495
+ # @example
496
+ # TODO: add examples
497
+ #
498
+ # @param [String] resource location
499
+ # @param [String] type of action
500
+ # @return [String] resource location
501
+ def trigger(resource_type_identifier, action)
502
+ # TODO: not tested
503
+ if @model.kinds.select { |kind| kind.term == resource_type }.any?
504
+ type_identifier = @model.kinds.select {
505
+ |kind| kind.term == resource_type_identifier
506
+ }.first.type_identifier
507
+
508
+ location = @model.get_by_id(type_identifier).location
509
+ resource_type_identifier = @endpoint + location
510
+ end
511
+ # check some basic pre-conditions
512
+ raise "Endpoint is not connected!" unless @connected
513
+ raise "Unknown resource identifier! #{resource_type_identifier}" unless resource_type_identifier.start_with? @endpoint
514
+
515
+ # encapsulate the acion in a collection
516
+ collection = Occi::Collection.new
517
+ scheme, term = action.split(' #')
518
+ collection.actions << Occi::Core::Action.new(scheme + '#', term)
519
+
520
+ # make the request
521
+ path = sanitize_resource_link(resource_type_identifier) + '?action=' + term
522
+ post path, collection
523
+ end
524
+
525
+ # Refreshes the Occi::Model used inside the client. Useful for
526
+ # updating the model without creating a new instance or
527
+ # reconnecting. Saves a lot of time in an interactive mode.
528
+ #
529
+ # @example
530
+ # client.refresh
531
+ def refresh
532
+ # re-download the model from the server
533
+ set_model
534
+ end
535
+
536
+ private
537
+
538
+ # Sets the logger and log levels. This allows users to pass existing logger
539
+ # instances to the rOCCI client.
540
+ #
541
+ # @example
542
+ # set_logger { :out => STDERR, :level => Occi::Log::WARN, :logger => nil }
543
+ #
544
+ # @param [Hash] logger options
545
+ def set_logger(log_options)
546
+ if log_options[:logger].nil? or (not log_options[:logger].kind_of? Occi::Log)
547
+ @logger = Occi::Log.new(log_options[:out])
548
+ @logger.level = log_options[:level]
549
+ end
550
+
551
+ self.class.debug_output $stderr if log_options[:level] == Occi::Log::DEBUG
552
+ end
553
+
554
+ # Sets auth method and appropriate httparty attributes. Supported auth methods
555
+ # are: ["basic", "digest", "x509", "none"]
556
+ #
557
+ # @example
558
+ # change_auth { :type => "none" }
559
+ # change_auth { :type => "basic", :username => "123", :password => "321" }
560
+ # change_auth { :type => "digest", :username => "123", :password => "321" }
561
+ # change_auth { :type => "x509", :user_cert => "~/cert.pem",
562
+ # :user_cert_password => "321", :ca_path => nil }
563
+ # change_auth { :type => "keystone", :token => "005c8a5d7f2c437a9999302c458afbda" }
564
+ #
565
+ # @param [Hash] authentication options
566
+ def change_auth(auth_options)
567
+ @auth_options = auth_options
568
+
569
+ case @auth_options[:type]
570
+ when "basic"
571
+ # set up basic auth
572
+ raise ArgumentError, "Missing required options 'username' and 'password' for basic auth!" unless @auth_options[:username] and @auth_options[:password]
573
+ self.class.basic_auth @auth_options[:username], @auth_options[:password]
574
+ when "digest"
575
+ # set up digest auth
576
+ raise ArgumentError, "Missing required options 'username' and 'password' for digest auth!" unless @auth_options[:username] and @auth_options[:password]
577
+ self.class.digest_auth @auth_options[:username], @auth_options[:password]
578
+ when "x509"
579
+ # set up pem and optionally pem_password and ssl_ca_path
580
+ raise ArgumentError, "Missing required option 'user_cert' for x509 auth!" unless @auth_options[:user_cert]
581
+ raise ArgumentError, "The file specified in 'user_cert' does not exist!" unless File.exists? @auth_options[:user_cert]
582
+
583
+ self.class.pem File.read(@auth_options[:user_cert]), @auth_options[:user_cert_password]
584
+ self.class.ssl_ca_path @auth_options[:ca_path] unless @auth_options[:ca_path].nil?
585
+ self.class.ssl_ca_file @auth_options[:ca_file] unless @auth_options[:ca_file].nil?
586
+ self.class.ssl_extra_chain_cert certs_to_file_ary(@auth_options[:proxy_ca]) unless @auth_options[:proxy_ca].nil?
587
+ when "keystone"
588
+ # set up OpenStack Keystone token based auth
589
+ raise ArgumentError, "Missing required option 'token' for OpenStack Keystone auth!" unless @auth_options[:token]
590
+ self.class.headers['X-Auth-Token'] = @auth_options[:token]
591
+ when "none", nil
592
+ # do nothing
593
+ else
594
+ raise ArgumentError, "Unknown AUTH method [#{@auth_options[:type]}]!"
595
+ end
596
+ end
597
+
598
+ # Reads X.509 certificates from a file to an array.
599
+ #
600
+ # @example
601
+ # certs_to_file_ary "~/.globus/usercert.pem"
602
+ # # => [#<String>, #<String>, ...]
603
+ #
604
+ # @param [String] Path to a PEM file containing certificates
605
+ # @return [Array<String>] An array of read certificates
606
+ def certs_to_file_ary(ca_file)
607
+ # TODO: read and separate multiple certificates
608
+ [] << File.read(ca_file)
609
+ end
610
+
611
+ # Performs GET request and parses the responses to collections.
612
+ #
613
+ # @example
614
+ # get "/-/" # => #<Occi::Collection>
615
+ # get "/compute/" # => #<Occi::Collection>
616
+ # get "/compute/fs65g4fs6g-sf54g54gsf-aa12faddf52" # => #<Occi::Collection>
617
+ #
618
+ # @param [String] path for the GET request
619
+ # @param [Occi::Collection] collection of filters
620
+ # @return [Occi::Collection] parsed result of the request
621
+ def get(path='', filter=nil)
622
+ # remove the leading slash
623
+ path.gsub!(/\A\//, '')
624
+
625
+ response = if filter
626
+ categories = filter.categories.collect { |category| category.to_text }.join(',')
627
+ attributes = filter.entities.collect { |entity| entity.attributes.combine.collect { |k, v| k + '=' + v } }.join(',')
628
+
629
+ headers = self.class.headers.clone
630
+ headers['Content-Type'] = 'text/occi'
631
+ headers['Category'] = categories unless categories.empty?
632
+ headers['X-OCCI-Attributes'] = attributes unless attributes.empty?
633
+
634
+ self.class.get(@endpoint + path, :headers => headers)
635
+ else
636
+ self.class.get(@endpoint + path)
637
+ end
638
+
639
+ response_msg = response_message response
640
+ raise "HTTP GET failed! #{response_msg}" unless response.code.between? 200, 300
641
+
642
+ Occi::Log.debug "Response location: #{('/' + path).match(/\/.*\//).to_s}"
643
+ kind = @model.get_by_location(('/' + path).match(/\/.*\//).to_s) if @model
644
+
645
+ Occi::Log.debug "Response kind: #{kind}"
646
+
647
+ if kind
648
+ kind.related_to? Occi::Core::Resource ? entity_type = Occi::Core::Resource : entity_type = nil
649
+ entity_type = Occi::Core::Link if kind.related_to? Occi::Core::Link
650
+ end
651
+
652
+ Occi::Log.debug "Parser call: #{response.content_type} #{entity_type} #{path.include?('-/')}"
653
+ collection = Occi::Parser.parse(response.content_type, response.body, path.include?('-/'), entity_type, response.headers)
654
+
655
+ Occi::Log.debug "Parsed collection: empty? #{collection.empty?}"
656
+ collection
657
+ end
658
+
659
+ # Performs POST requests and returns URI locations. Resource data must be provided
660
+ # in an Occi::Collection instance.
661
+ #
662
+ # @example
663
+ # collection = Occi::Collection.new
664
+ # collection.resources << entity if entity.kind_of? Occi::Core::Resource
665
+ # collection.links << entity if entity.kind_of? Occi::Core::Link
666
+ #
667
+ # post "/compute/", collection # => "http://localhost:3300/compute/23sf4g65as-asdgsg2-sdfgsf2g"
668
+ # post "/network/", collection # => "http://localhost:3300/network/23sf4g65as-asdgsg2-sdfgsf2g"
669
+ # post "/storage/", collection # => "http://localhost:3300/storage/23sf4g65as-asdgsg2-sdfgsf2g"
670
+ #
671
+ # @param [String] path for the POST request
672
+ # @param [Occi::Collection] resource data to be POSTed
673
+ # @return [String] URI location
674
+ def post(path, collection)
675
+ # remove the leading slash
676
+ path.gsub!(/\A\//, '')
677
+
678
+ response = if @media_type == 'application/occi+json'
679
+ self.class.post(@endpoint + path,
680
+ :body => collection.to_json,
681
+ :headers => { 'Accept' => 'text/uri-list', 'Content-Type' => 'application/occi+json' })
682
+ elsif @media_type == 'text/occi'
683
+ self.class.post(@endpoint + path,
684
+ :headers => collection.to_header.merge({ 'Accept' => 'text/uri-list', 'Content-Type' => 'text/occi' }))
685
+ else
686
+ self.class.post(@endpoint + path,
687
+ :body => collection.to_text,
688
+ :headers => { 'Accept' => 'text/uri-list', 'Content-Type' => 'text/plain' })
689
+ end
690
+
691
+ response_msg = response_message response
692
+ raise "HTTP POST failed! #{response_msg}" unless response.code.between? 200, 300
693
+
694
+ URI.parse(response.body).to_s
695
+ end
696
+
697
+ # Performs PUT requests and parses responses to collections.
698
+ #
699
+ # @example
700
+ # TODO: add examples
701
+ #
702
+ # @param [String] path for the PUT request
703
+ # @param [Occi::Collection] resource data to send
704
+ # @return [Occi::Collection] parsed result of the request
705
+ def put(path, collection)
706
+ # remove the leading slash
707
+ path.gsub!(/\A\//, '')
708
+
709
+ response = if @media_type == 'application/occi+json'
710
+ self.class.post(@endpoint + path, :body => collection.to_json, :headers => { 'Content-Type' => 'application/occi+json' })
711
+ else
712
+ self.class.post(@endpoint + path, { :body => collection.to_text, :headers => { 'Content-Type' => 'text/plain' } })
713
+ end
714
+
715
+ response_msg = response_message response
716
+ raise "HTTP PUT failed! #{response_msg}" unless response.code.between? 200, 300
717
+
718
+ collection = Occi::Parser.parse(response.content_type, response.body)
719
+
720
+ collection
721
+ end
722
+
723
+ # Performs DELETE requests and returns True on success.
724
+ #
725
+ # @example
726
+ # del "/compute/65sf4g65sf4g-sf6g54sf5g-sfgsf32g3" # => true
727
+ #
728
+ # @param [String] path for the DELETE request
729
+ # @param [Occi::Collection] collection of filters (currently NOT used)
730
+ # @return [Boolean] status
731
+ def del(path, filter=nil)
732
+ # remove the leading slash
733
+ path.gsub!(/\A\//, '')
734
+
735
+ response = self.class.delete(@endpoint + path)
736
+
737
+ response_msg = response_message response
738
+ raise "HTTP DELETE failed! #{response_msg}" unless response.code.between? 200, 300
739
+
740
+ true
741
+ end
742
+
743
+ # Creates a link of a specified kind and binds it to the given resource.
744
+ #
745
+ # @example
746
+ # link_kind = 'http://schemas.ogf.org/occi/infrastructure#storagelink'
747
+ # compute = client.get_resource "compute"
748
+ # storage_location = "http://localhost:3300/storage/321df21adfad-f3adfa5f4adf-a3d54ffadffe"
749
+ # linked_resource_kind = 'http://schemas.ogf.org/occi/infrastructure#storage'
750
+ #
751
+ # link link_kind, compute, storage_location, linked_resource_kind
752
+ #
753
+ # @param [String] link type identifier (link kind)
754
+ # @param [Occi::Core::Resource] resource to link to
755
+ # @param [URI, String] resource to be linked
756
+ # @param [String] type identifier of the linked resource
757
+ # @param [Occi::Core::Attributes] link attributes
758
+ # @param [Array<String>] link mixins
759
+ # @return [Occi::Core::Link] link instance
760
+ def link(kind, source, target_location, target_kind, attributes=Occi::Core::Attributes.new, mixins=[])
761
+ link = Occi::Core::Link.new(kind)
762
+ link.mixins = mixins
763
+ link.attributes = attributes
764
+ link.target = (target_location.kind_of? URI::Generic) ? target_location.path : target_location.to_s
765
+ link.rel = target_kind
766
+
767
+ jj link
768
+ link.check @model
769
+ source.links << link
770
+
771
+ link
772
+ end
773
+
774
+ # Checks whether the given endpoint URI is valid and adds a trailing
775
+ # slash if necessary.
776
+ #
777
+ # @example
778
+ # prepare_endpoint "http://localhost:3300" # => "http://localhost:3300/"
779
+ #
780
+ # @param [String] endpoint URI in a non-canonical string
781
+ # @return [String] canonical endpoint URI in a string, with a trailing slash
782
+ def prepare_endpoint(endpoint)
783
+ raise 'Endpoint not a valid URI' if (endpoint =~ URI::ABS_URI).nil?
784
+ @endpoint = endpoint.chomp('/') + '/'
785
+ end
786
+
787
+ # Extracts the resource path from a resource link. It will remove the leading @endpoint
788
+ # and replace it with a slash.
789
+ #
790
+ # @example
791
+ # sanitize_resource_link "http://localhost:3300/compute/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
792
+ # # => "/compute/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
793
+ #
794
+ # @param [String] string containing the full resource link
795
+ # @return [String] extracted path, with a leading slash
796
+ def sanitize_resource_link(resource_link)
797
+ raise "Resource link #{resource_link} is not valid!" unless resource_link.start_with? @endpoint
798
+
799
+ resource_link.gsub @endpoint, '/'
800
+ end
801
+
802
+ # @describe find the path for the resource type identifier
803
+ #
804
+ # @example
805
+ #
806
+ #
807
+ # @param [String] resource_type_identifier
808
+ #
809
+ # @return [String]
810
+ def path_for_resource_type(resource_type_identifier)
811
+ if resource_type_identifier.nil? || resource_type_identifier == "/"
812
+ #we got all
813
+ path = "/"
814
+ else
815
+ kinds = @model.kinds.select { |kind| kind.term == resource_type_identifier }
816
+ if kinds.any?
817
+ #we got an type identifier
818
+ path = "/" + kinds.first.type_identifier.split('#').last + "/"
819
+ elsif resource_type_identifier.start_with? @endpoint
820
+ #we got an resource link
821
+ path = sanitize_resource_link(resource_type_identifier)
822
+ else
823
+ raise "Unknown resource identifier! #{resource_type_identifier}"
824
+ end
825
+ end
826
+
827
+ path
828
+ end
829
+
830
+ # Creates an Occi::Model from data retrieved from the server.
831
+ #
832
+ # @example
833
+ # set_model
834
+ def set_model
835
+
836
+ #
837
+ model = get('/-/')
838
+ @model = Occi::Model.new(model)
839
+
840
+ @mixins = {
841
+ :os_tpl => [],
842
+ :resource_tpl => []
843
+ }
844
+
845
+ #
846
+ get_os_templates.each do |os_tpl|
847
+ @mixins[:os_tpl] << os_tpl.type_identifier unless os_tpl.nil? or os_tpl.type_identifier.nil?
848
+ end
849
+
850
+ #
851
+ get_resource_templates.each do |res_tpl|
852
+ @mixins[:resource_tpl] << res_tpl.type_identifier unless res_tpl.nil? or res_tpl.type_identifier.nil?
853
+ end
854
+ end
855
+
856
+ # Retrieves available os_tpls from the model.
857
+ #
858
+ # @example
859
+ # get_os_templates # => #<Occi::Collection>
860
+ #
861
+ # @return [Occi::Collection] collection containing all registered OS templates
862
+ def get_os_templates
863
+ @model.get.mixins.select { |mixin| mixin.related.select { |rel| rel.end_with? 'os_tpl' }.any? }
864
+ end
865
+
866
+ # Retrieves available resource_tpls from the model.
867
+ #
868
+ # @example
869
+ # get_resource_templates # => #<Occi::Collection>
870
+ #
871
+ # @return [Occi::Collection] collection containing all registered resource templates
872
+ def get_resource_templates
873
+ @model.get.mixins.select { |mixin| mixin.related.select { |rel| rel.end_with? 'resource_tpl' }.any? }
874
+ end
875
+
876
+ # Sets media type. Will choose either application/occi+json or text/plain
877
+ # based on the formats supported by the server.
878
+ #
879
+ # @example
880
+ # set_media_type # => 'application/occi+json'
881
+ #
882
+ # @return [String] chosen media type
883
+ def set_media_type
884
+ media_types = self.class.head(@endpoint).headers['accept']
885
+ Occi::Log.debug("Available media types: #{media_types}")
886
+ @media_type = case media_types
887
+ when /application\/occi\+json/
888
+ 'application/occi+json'
889
+ else
890
+ 'text/plain'
891
+ end
892
+ end
893
+
894
+ # Generates a human-readable response message based on the HTTP response code.
895
+ #
896
+ # @example
897
+ # response_message self.class.delete(@endpoint + path)
898
+ # # => 'HTTP Response status: [200] OK'
899
+ #
900
+ # @param [HTTParty::Response] HTTParty response object
901
+ # @return [String] message
902
+ def response_message(response)
903
+ @last_response = response
904
+ 'HTTP Response status: [' + response.code.to_s + '] ' + reason_phrase(response.code)
905
+ end
906
+
907
+ # Converts HTTP response codes to human-readable phrases.
908
+ #
909
+ # @example
910
+ # reason_phrase(500) # => "Internal Server Error"
911
+ #
912
+ # @param [Integer] HTTP response code
913
+ # @return [String] human-readable phrase
914
+ def reason_phrase(code)
915
+ HTTP_CODES[code.to_s]
916
+ end
917
+
918
+ end
919
+
920
+ end
921
+ end
922
+ end