occi 2.5.3 → 2.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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