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