occi 2.5.3 → 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,595 @@
1
+ require 'rubygems'
2
+ require 'httparty'
3
+
4
+ module OCCI
5
+ class Client
6
+
7
+ # HTTParty for raw HTTP requests
8
+ include HTTParty
9
+ headers 'Accept' => 'application/occi+json,text/plain;q=0.5'
10
+
11
+ # a few attributes which should be visible outside the client
12
+ attr_reader :endpoint
13
+ attr_reader :auth_options
14
+ attr_reader :media_type
15
+ attr_reader :connected
16
+ attr_reader :model
17
+
18
+ # hash mapping human-readable resource names to OCCI identifiers
19
+ # TODO: get resources dynamically from the model
20
+ RESOURCES = {
21
+ :compute => "http://schemas.ogf.org/occi/infrastructure#compute",
22
+ :storage => "http://schemas.ogf.org/occi/infrastructure#storage",
23
+ :network => "http://schemas.ogf.org/occi/infrastructure#network"
24
+ }
25
+
26
+ # hash mapping HTTP response codes to human-readable messages
27
+ HTTP_CODES = {
28
+ "100" => "Continue",
29
+ "101" => "Switching Protocols",
30
+ "200" => "OK",
31
+ "201" => "Created",
32
+ "202" => "Accepted",
33
+ "203" => "Non-Authoritative Information",
34
+ "204" => "No Content",
35
+ "205" => "Reset Content",
36
+ "206" => "Partial Content",
37
+ "300" => "Multiple Choices",
38
+ "301" => "Moved Permanently",
39
+ "302" => "Found",
40
+ "303" => "See Other",
41
+ "304" => "Not Modified",
42
+ "305" => "Use Proxy",
43
+ "307" => "Temporary Redirect",
44
+ "400" => "Bad Request",
45
+ "401" => "Unauthorized",
46
+ "402" => "Payment Required",
47
+ "403" => "Forbidden",
48
+ "404" => "Not Found",
49
+ "405" => "Method Not Allowed",
50
+ "406" => "Not Acceptable",
51
+ "407" => "Proxy Authentication Required",
52
+ "408" => "Request Time-out",
53
+ "409" => "Conflict",
54
+ "410" => "Gone",
55
+ "411" => "Length Required",
56
+ "412" => "Precondition Failed",
57
+ "413" => "Request Entity Too Large",
58
+ "414" => "Request-URI Too Large",
59
+ "415" => "Unsupported Media Type",
60
+ "416" => "Requested range not satisfiable",
61
+ "417" => "Expectation Failed",
62
+ "500" => "Internal Server Error",
63
+ "501" => "Not Implemented",
64
+ "502" => "Bad Gateway",
65
+ "503" => "Service Unavailable",
66
+ "504" => "Gateway Time-out",
67
+ "505" => "HTTP Version not supported"
68
+ }
69
+
70
+ # @param [String] Endpoint URI
71
+ # @param [Hash] Auth options
72
+ # @param [Hash] Logging options
73
+ # @param [Boolean] Enable autoconnect?
74
+ # @return [OCCI:Client] Client instance
75
+ def initialize(endpoint = "http://localhost:3000/", auth_options = {:type => "none"}, log_options = { :out => STDERR, :level => OCCI::Log::WARN, :logger => nil}, auto_connect = true, media_type = nil)
76
+ # set OCCI::Log
77
+ set_logger log_options
78
+
79
+ # pass auth options to HTTParty
80
+ change_auth auth_options
81
+
82
+ # check the validity and canonize the endpoint URI
83
+ prepare_endpoint endpoint
84
+
85
+ # get accepted media types from HTTParty
86
+ set_media_type
87
+
88
+ # force media_type if provided
89
+ if media_type
90
+ self.class.headers 'Accept' => media_type
91
+ @media_type = media_type
92
+ end
93
+
94
+ OCCI::Log.debug("Media Type: #{@media_type}")
95
+ OCCI::Log.debug("Headers: #{self.class.headers}")
96
+
97
+ # get model information from the endpoint
98
+ # and create OCCI::Model instance
99
+ set_model
100
+
101
+ # auto-connect?
102
+ @connected = auto_connect
103
+ end
104
+
105
+ # @param [String] Resource name or resource identifier
106
+ # @return [OCCI::Core::Entity] Resource instance
107
+ def get_resource(resource_type)
108
+
109
+ OCCI::Log.debug("Instantiating #{resource_type} ...")
110
+
111
+ if RESOURCES.has_value? resource_type
112
+ # we got a resource type identifier
113
+ OCCI::Core::Resource.new resource_type
114
+ elsif RESOURCES.has_key? resource_type.to_sym
115
+ # we got a resource type name
116
+ OCCI::Core::Resource.new get_resource_type_identifier(resource_type)
117
+ else
118
+ raise "Unknown resource type! [#{resource_type}]"
119
+ end
120
+
121
+ end
122
+
123
+ # @return [Array] List of available resource types in a human-readable format
124
+ def get_resource_types
125
+ OCCI::Log.debug("Getting resource types ...")
126
+ RESOURCES.keys.map! { |k| k.to_s }
127
+ end
128
+
129
+ # @return [Array] List of available resource types in a OCCI ID format
130
+ def get_resource_type_identifiers
131
+ OCCI::Log.debug("Getting resource identifiers ...")
132
+ RESOURCES.values
133
+ end
134
+
135
+ # @param [String] Name of the mixin
136
+ # @param [String] Type of the mixin
137
+ # @param [Boolean] Should we describe the mixin or return its link?
138
+ # @return [String, OCCI:Collection] Link or mixin description
139
+ def find_mixin(name, type = nil, describe = false)
140
+
141
+ OCCI::Log.debug("Looking for mixin #{name} + #{type} + #{describe}")
142
+
143
+ # is type valid?
144
+ unless type.nil?
145
+ raise "Unknown mixin type! [#{type}]" unless @mixins.has_key? type.to_sym
146
+ end
147
+
148
+ # TODO: extend this code to support multiple matches and regex filters
149
+ # should we look for links or descriptions?
150
+ unless describe
151
+ # we are looking for links
152
+ # prefix mixin name with '#' to simplify the search
153
+ name = "#" + name
154
+ unless type
155
+ # there is no type preference, return first global match
156
+ @mixins.flatten(2).select { |mixin| mixin.to_s.reverse.start_with? name.reverse }.first
157
+ else
158
+ # return the first match with the selected type
159
+ @mixins[type.to_sym].select { |mixin| mixin.to_s.reverse.start_with? name.reverse }.first
160
+ end
161
+ else
162
+ # we are looking for descriptions
163
+ unless type
164
+ # try in os_tpls first
165
+ found = get_os_templates.select { |mixin| mixin.term == name }.first
166
+
167
+ # then try in resource_tpls
168
+ unless found
169
+ found = get_resource_templates.select { |template| template.term == name }.first
170
+ end
171
+
172
+ found
173
+ else
174
+ # get the first match from either os_tpls or resource_tpls
175
+ case
176
+ when type == "os_tpl"
177
+ get_os_templates.select { |mixin| mixin.term == name }.first
178
+ when type == "resource_tpl"
179
+ get_resource_templates.select { |template| template.term == name }.first
180
+ else
181
+ nil
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ # @param [String] Type of mixins to return
188
+ # @return [Array] List of available mixins
189
+ def get_mixins(type = nil)
190
+ unless type.nil?
191
+ # is type valid?
192
+ raise "Unknown mixin type! #{type}" unless @mixins.has_key? type.to_sym
193
+
194
+ # return mixin of the selected type
195
+ @mixins[type.to_sym]
196
+ else
197
+ # we did not get a type, return all mixins
198
+ mixins = []
199
+
200
+ # flatten the hash and remove its keys
201
+ get_mixin_types.each do |type|
202
+ mixins.concat @mixins[type.to_sym]
203
+ end
204
+
205
+ mixins
206
+ end
207
+ end
208
+
209
+ # @return [Array] List of available mixin types
210
+ def get_mixin_types
211
+ @mixins.keys.map! { |k| k.to_s }
212
+ end
213
+
214
+ # @return [Array] List of available mixin type identifiers
215
+ def get_mixin_type_identifiers
216
+ identifiers = []
217
+
218
+ get_mixin_types.each do |mixin_type|
219
+ identifiers << 'http://schemas.ogf.org/occi/infrastructure#' + mixin_type
220
+ end
221
+
222
+ identifiers
223
+ end
224
+
225
+ # @param [String] Human-readable name of the resource
226
+ # @return [String] OCCI resource identifier
227
+ def get_resource_type_identifier(resource_type)
228
+ raise "Unknown resource type! [#{resource_type}]" unless RESOURCES.has_key? resource_type.to_sym
229
+
230
+ RESOURCES[resource_type.to_sym]
231
+ end
232
+
233
+ # @param [String] OCCI resource identifier
234
+ # @return [String] Human-readable name of the resource
235
+ def get_resource_type(resource_type_identifier)
236
+ raise "Unknown resource type identifier! [#{resource_type_identifier}]" unless RESOURCES.has_value? resource_type_identifier
237
+
238
+ RESOURCES.key(resource_type_identifier).to_s
239
+ end
240
+
241
+ # @param [String] OCCI resource type identifier or just type
242
+ # @return [Array] List of links
243
+ def list(resource_type_identifier)
244
+
245
+ # convert type to type identifier
246
+ unless resource_type_identifier.start_with? "http://" or resource_type_identifier.start_with? "https://"
247
+ resource_type_identifier = get_resource_type_identifier resource_type_identifier
248
+ end
249
+
250
+ # check some basic pre-conditions
251
+ raise "Endpoint is not connected!" unless @connected
252
+ raise "Unkown resource type identifier! [#{resource_type_identifier}]" unless RESOURCES.has_value? resource_type_identifier
253
+
254
+ # split the type identifier and get the most important part
255
+ uri_part = resource_type_identifier.split('#').last
256
+
257
+ list = []
258
+
259
+ # request uri-list from the server
260
+ path = uri_part + '/'
261
+ list = self.class.get(@endpoint + path, :headers => { "Accept" => 'text/uri-list' }).body.split("\n").compact
262
+
263
+ list
264
+ end
265
+
266
+ # @param [String] OCCI resource type identifier or just type
267
+ # @return [OCCI::Collection] List of descriptions
268
+ def describe(resource_identifier)
269
+
270
+ # convert type to type identifier
271
+ unless resource_identifier.start_with? "http://" or resource_identifier.start_with? "https://"
272
+ resource_identifier = get_resource_type_identifier resource_identifier
273
+ end
274
+
275
+ # check some basic pre-conditions
276
+ raise "Endpoint is not connected!" unless @connected
277
+
278
+ descriptions = nil
279
+
280
+ if RESOURCES.has_value? resource_identifier
281
+ # we got type identifier
282
+ # split the type identifier
283
+ uri_part = resource_identifier.split('#').last
284
+ # make the request
285
+ descriptions = get(uri_part + '/')
286
+ elsif resource_identifier.start_with? @endpoint
287
+ # we got resource link
288
+ # make the request
289
+ descriptions = get(sanitize_resource_link(resource_identifier))
290
+ else
291
+ raise "Unkown resource identifier! [#{resource_identifier}]"
292
+ end
293
+
294
+ descriptions
295
+ end
296
+
297
+ # @param [OCCI::Core::Entity] Entity to be created on the server
298
+ # @return [String] Link (URI) or the new resource
299
+ def create(entity)
300
+
301
+ # check some basic pre-conditions
302
+ raise "Endpoint is not connected!" unless @connected
303
+ raise "#{entity} not an entity" unless entity.kind_of? OCCI::Core::Entity
304
+
305
+ # is this entity valid?
306
+ entity.check(@model)
307
+ kind = @model.get_by_id(entity.kind)
308
+ raise "No kind found for #{entity}" unless kind
309
+
310
+ # get location for this kind of entity
311
+ location = @model.get_by_id(entity.kind).location
312
+ collection = OCCI::Collection.new
313
+
314
+ # is this entity a Resource or a Link?
315
+ collection.resources << entity if entity.kind_of? OCCI::Core::Resource
316
+ collection.links << entity if entity.kind_of? OCCI::Core::Link
317
+
318
+ # make the request
319
+ post location, collection
320
+ end
321
+
322
+ # @param [String] Resource link (URI)
323
+ # @return [Boolean] Success?
324
+ def delete(resource_identifier)
325
+ # TODO: delete should work for entire resource types
326
+ # check some basic pre-conditions
327
+ raise "Endpoint is not connected!" unless @connected
328
+ raise "Unknown resource identifier! #{resource_identifier}" unless resource_identifier.start_with? @endpoint
329
+
330
+ # make the request
331
+ del(sanitize_resource_link(resource_identifier))
332
+ end
333
+
334
+ # @param [String] Resource link (URI)
335
+ # @param [String] Type of action
336
+ # @return [String] Resource link (URI)
337
+ def trigger(resource_identifier, action)
338
+ # TODO: not tested
339
+ # check some basic pre-conditions
340
+ raise "Endpoint is not connected!" unless @connected
341
+ raise "Unknown resource identifier! #{resource_identifier}" unless resource_identifier.start_with? @endpoint
342
+
343
+ # encapsulate the acion in a collection
344
+ collection = OCCI::Collection.new
345
+ collection.actions << action
346
+
347
+ # make the request
348
+ post sanitize_resource_link(resource_identifier), collection
349
+ end
350
+
351
+ def refresh
352
+ # re-download the model from the server
353
+ set_model
354
+ end
355
+
356
+ # @param [OCCI::Core::Resource] Compute instance
357
+ # @param [URI,String] Storage location (URI)
358
+ # @param [OCCI::Core::Attributes] Attributes
359
+ # @param [Array] Mixins
360
+ # @return [OCCI::Core::Link] Link instance
361
+ def storagelink(compute, storage_location, attributes=OCCI::Core::Attributes.new, mixins=[])
362
+ kind = 'http://schemas.ogf.org/occi/infrastructure#storagelink'
363
+ storage_kind = 'http://schemas.ogf.org/occi/infrastructure#storage'
364
+ storagelink = link(kind, compute, storage_location, storage_kind, attributes, mixins)
365
+
366
+ storagelink
367
+ end
368
+
369
+ # @param [OCCI::Core::Resource] Compute instance
370
+ # @param [URI,String] Network location (URI)
371
+ # @param [OCCI::Core::Attributes] Attributes
372
+ # @param [Array] Mixins
373
+ # @return [OCCI::Core::Link] Link instance
374
+ def networkinterface(compute, network_location, attributes=OCCI::Core::Attributes.new, mixins=[])
375
+ kind = 'http://schemas.ogf.org/occi/infrastructure#networkinterface'
376
+ network_kind = 'http://schemas.ogf.org/occi/infrastructure#network'
377
+ networkinterface = link(kind, compute, network_location, network_kind, attributes, mixins)
378
+
379
+ networkinterface
380
+ end
381
+
382
+ #private
383
+
384
+ # @param [Hash]
385
+ def set_logger(log_options)
386
+
387
+ if log_options[:logger].nil? or not (log_options[:logger].kind_of? OCCI::Log)
388
+ logger = OCCI::Log.new(log_options[:out])
389
+ logger.level = log_options[:level]
390
+ end
391
+
392
+ self.class.debug_output $stderr if log_options[:level] == OCCI::Log::DEBUG
393
+
394
+ end
395
+
396
+ # @param [Hash]
397
+ def change_auth(auth_options)
398
+ @auth_options = auth_options
399
+
400
+ case @auth_options[:type]
401
+ when "basic"
402
+ # set up basic auth
403
+ raise ArgumentError, "Missing required options 'username' and 'password' for basic auth!" unless @auth_options[:username] and @auth_options[:password]
404
+ self.class.basic_auth @auth_options[:username], @auth_options[:password]
405
+ when "digest"
406
+ # set up digest auth
407
+ raise ArgumentError, "Missing required options 'username' and 'password' for digest auth!" unless @auth_options[:username] and @auth_options[:password]
408
+ self.class.digest_auth @auth_options[:username], @auth_options[:password]
409
+ when "x509"
410
+ # set up pem and optionally pem_password and ssl_ca_path
411
+ raise ArgumentError, "Missing required option 'user_cert' for x509 auth!" unless @auth_options[:user_cert]
412
+ raise ArgumentError, "The file specified in 'user_cert' does not exist!" unless File.exists? @auth_options[:user_cert]
413
+
414
+ self.class.pem File.read(@auth_options[:user_cert]), @auth_options[:user_cert_password]
415
+ self.class.ssl_ca_path @auth_options[:ca_path] unless @auth_options[:ca_path].nil? or @auth_options[:ca_path].empty?
416
+ when "none", nil
417
+ # do nothing
418
+ else
419
+ raise ArgumentError, "Unknown AUTH method [#{@auth_options[:type]}]!"
420
+ end
421
+ end
422
+
423
+ # @param [String]
424
+ # @param [OCCI::Collection]
425
+ # @return [OCCI::Collection]
426
+ def get(path='', filter=nil)
427
+ path = path.reverse.chomp('/').reverse
428
+ response = if filter
429
+ categories = filter.categories.collect { |category| category.to_text }.join(',')
430
+ attributes = filter.entities.collect { |entity| entity.attributes.combine.collect { |k, v| k + '=' + v } }.join(',')
431
+ headers = self.class.headers.clone
432
+ headers['Content-Type'] = 'text/occi'
433
+ headers['Category'] = categories unless categories.empty?
434
+ headers['X-OCCI-Attributes'] = attributes unless attributes.empty?
435
+ self.class.get(@endpoint + path,
436
+ :headers => headers)
437
+ else
438
+ self.class.get(@endpoint + path)
439
+ end
440
+
441
+ response_msg = response_message response
442
+ raise "HTTP GET failed! #{response_msg}" unless response.code.between? 200, 300
443
+
444
+ kind = @model.get_by_location path if @model
445
+ kind ? entity_type = kind.entity_type : entity_type = nil
446
+ _, collection = OCCI::Parser.parse(response.content_type, response.body, path.include?('-/'), entity_type)
447
+
448
+ collection
449
+ end
450
+
451
+ # @param [String]
452
+ # @param [OCCI::Collection]
453
+ # @return [String]
454
+ def post(path, collection)
455
+ path = path.reverse.chomp('/').reverse
456
+ response = if @media_type == 'application/occi+json'
457
+ self.class.post(@endpoint + path,
458
+ :body => collection.to_json,
459
+ :headers => { 'Accept' => 'text/uri-list', 'Content-Type' => 'application/occi+json' })
460
+ else
461
+ self.class.post(@endpoint + path,
462
+ :body => collection.to_text,
463
+ :headers => { 'Accept' => 'text/uri-list', 'Content-Type' => 'text/plain' })
464
+ end
465
+
466
+ response_msg = response_message response
467
+ raise "HTTP POST failed! #{response_msg}" unless response.code.between? 200, 300
468
+
469
+ URI.parse(response.body).to_s
470
+ end
471
+
472
+ # @param [String]
473
+ # @param [OCCI::Collection]
474
+ # @return [OCCI::Collection]
475
+ def put(path, collection)
476
+ path = path.reverse.chomp('/').reverse
477
+ response = if @media_type == 'application/occi+json'
478
+ self.class.post(@endpoint + path, :body => collection.to_json, :headers => { 'Content-Type' => 'application/occi+json' })
479
+ else
480
+ self.class.post(@endpoint + path, { :body => collection.to_text, :headers => { 'Content-Type' => 'text/plain' } })
481
+ end
482
+
483
+ response_msg = response_message response
484
+ raise "HTTP PUT failed! #{response_msg}" unless response.code.between? 200, 300
485
+
486
+ _, collection = OCCI::Parser.parse(response.content_type, response.body)
487
+
488
+ collection
489
+ end
490
+
491
+ # @param [String]
492
+ # @param [OCCI::Collection]
493
+ # @return [Boolean]
494
+ def del(path, collection=nil)
495
+ path = path.reverse.chomp('/').reverse
496
+ response = self.class.delete(@endpoint + path)
497
+
498
+ response_msg = response_message response
499
+ raise "HTTP DELETE failed! #{response_msg}" unless response.code.between? 200, 300
500
+
501
+ true
502
+ end
503
+
504
+ # @param [String]
505
+ # @param [OCCI::Core::Resource]
506
+ # @param [URI,String]
507
+ # @param [String]
508
+ # @param [OCCI::Core::Attributes]
509
+ # @param [Array]
510
+ # @return [OCCI::Core::Link]
511
+ def link(kind, source, target_location, target_kind, attributes=OCCI::Core::Attributes.new, mixins=[])
512
+ link = OCCI::Core::Link.new(kind)
513
+ link.mixins = mixins
514
+ link.attributes = attributes
515
+ link.target = (target_location.kind_of? URI::Generic) ? target_location.path : target_location.to_s
516
+ link.rel = target_kind
517
+
518
+ jj link
519
+ link.check @model
520
+ source.links << link
521
+ link
522
+ end
523
+
524
+ # @param [String]
525
+ # @return [String]
526
+ def prepare_endpoint(endpoint)
527
+ raise 'Endpoint not a valid URI' if (endpoint =~ URI::ABS_URI).nil?
528
+ @endpoint = endpoint.chomp('/') + '/'
529
+ end
530
+
531
+ # @param [String]
532
+ # @return [String]
533
+ def sanitize_resource_link(resource_link)
534
+ raise "Resource link #{resource_link} is not valid!" unless resource_link.start_with? @endpoint
535
+
536
+ resource_link.gsub @endpoint, '/'
537
+ end
538
+
539
+ def set_model
540
+
541
+ #
542
+ model = get('/-/')
543
+ @model = OCCI::Model.new(model)
544
+
545
+ @mixins = {
546
+ :os_tpl => [],
547
+ :resource_tpl => []
548
+ }
549
+
550
+ #
551
+ get_os_templates.each do |os_tpl|
552
+ @mixins[:os_tpl] << os_tpl.type_identifier unless os_tpl.nil? or os_tpl.type_identifier.nil?
553
+ end
554
+
555
+ #
556
+ get_resource_templates.each do |res_tpl|
557
+ @mixins[:resource_tpl] << res_tpl.type_identifier unless res_tpl.nil? or res_tpl.type_identifier.nil?
558
+ end
559
+ end
560
+
561
+ # @return [OCCI::Collection] collection including all registered OS templates
562
+ def get_os_templates
563
+ @model.get.mixins.select { |mixin| mixin.related.select { |rel| rel.end_with? 'os_tpl' }.any? }
564
+ end
565
+
566
+ # @return [OCCI::Collection] collection including all registered resource templates
567
+ def get_resource_templates
568
+ @model.get.mixins.select { |mixin| mixin.related.select { |rel| rel.end_with? 'resource_tpl' }.any? }
569
+ end
570
+
571
+ # @return [String]
572
+ def set_media_type
573
+ media_types = self.class.head(@endpoint).headers['accept']
574
+ OCCI::Log.debug("Available media types: #{media_types}")
575
+ @media_type = case media_types
576
+ when /application\/occi\+json/
577
+ 'application/occi+json'
578
+ else
579
+ 'text/plain'
580
+ end
581
+ end
582
+
583
+ # @param [HTTParty::Response]
584
+ def response_message(response)
585
+ 'HTTP Response status: [' + response.code.to_s + '] ' + reason_phrase(response.code)
586
+ end
587
+
588
+ # @param [Integer]
589
+ # @return [String]
590
+ def reason_phrase(code)
591
+ HTTP_CODES[code.to_s]
592
+ end
593
+
594
+ end
595
+ end