occi-api 4.2.0.beta.4 → 4.2.0.beta.6
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.
- data/lib/occi-api.rb +5 -1
- data/lib/occi/api/client/authn_utils.rb +84 -76
- data/lib/occi/api/client/base/category_methods.rb +54 -0
- data/lib/occi/api/client/base/entity_methods.rb +172 -0
- data/lib/occi/api/client/base/helpers.rb +91 -0
- data/lib/occi/api/client/base/kind_methods.rb +70 -0
- data/lib/occi/api/client/base/mixin_methods.rb +223 -0
- data/lib/occi/api/client/base/protected_helpers.rb +79 -0
- data/lib/occi/api/client/base/protected_stubs.rb +44 -0
- data/lib/occi/api/client/base/stubs.rb +142 -0
- data/lib/occi/api/client/client_base.rb +65 -860
- data/lib/occi/api/client/client_http.rb +181 -492
- data/lib/occi/api/client/errors.rb +0 -2
- data/lib/occi/api/client/http/authn_plugins.rb +3 -6
- data/lib/occi/api/client/http/code_helpers.rb +64 -0
- data/lib/occi/api/client/http/helpers.rb +99 -0
- data/lib/occi/api/client/http/monkey_patches.rb +2 -0
- data/lib/occi/api/client/http/{httparty_fix.rb → monkey_patches/httparty_fix.rb} +4 -0
- data/lib/occi/api/client/http/{net_http_fix.rb → monkey_patches/net_http_fix.rb} +0 -0
- data/lib/occi/api/client/http/party_wrappers.rb +173 -0
- data/lib/occi/api/dsl.rb +32 -193
- data/lib/occi/api/dsl/helper_methods.rb +22 -0
- data/lib/occi/api/dsl/main_methods.rb +47 -0
- data/lib/occi/api/dsl/mixin_methods.rb +32 -0
- data/lib/occi/api/dsl/type_methods.rb +102 -0
- data/lib/occi/api/version.rb +1 -1
- metadata +20 -4
@@ -1,884 +1,89 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module Client
|
1
|
+
# load all parts of the ClientBase
|
2
|
+
Dir[File.join(File.dirname(__FILE__), 'base', '*.rb')].each { |file| require file.gsub('.rb', '') }
|
4
3
|
|
5
|
-
|
4
|
+
module Occi::Api::Client
|
6
5
|
|
7
|
-
|
8
|
-
attr_reader :endpoint, :auth_options, :media_type
|
9
|
-
attr_reader :connected, :model, :logger, :last_response
|
10
|
-
attr_reader :options
|
6
|
+
class ClientBase
|
11
7
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
:log => {:out => STDERR, :level => Occi::Log::WARN, :logger => nil},
|
17
|
-
:auto_connect => true,
|
18
|
-
:media_type => nil
|
19
|
-
}
|
8
|
+
# a few attributes which should be visible outside the client
|
9
|
+
attr_reader :endpoint, :auth_options, :media_type
|
10
|
+
attr_reader :connected, :model, :logger, :last_response
|
11
|
+
attr_reader :options
|
20
12
|
|
21
|
-
|
22
|
-
|
13
|
+
def initialize(options = {})
|
14
|
+
# define defaults and convert options to Hashie::Mash if necessary
|
15
|
+
defaults = Hashie::Mash.new({
|
16
|
+
:endpoint => "http://localhost:3300/",
|
17
|
+
:auth => {:type => "none"},
|
18
|
+
:log => {:out => STDERR, :level => Occi::Log::WARN, :logger => nil},
|
19
|
+
:auto_connect => true,
|
20
|
+
:media_type => nil
|
21
|
+
})
|
23
22
|
|
24
|
-
|
25
|
-
|
23
|
+
options = options.marshal_dump if options.is_a?(OpenStruct)
|
24
|
+
options = Hashie::Mash.new(options)
|
26
25
|
|
27
|
-
|
28
|
-
@endpoint = get_endpoint_uri(@options[:endpoint])
|
26
|
+
@options = defaults.merge(options)
|
29
27
|
|
30
|
-
|
31
|
-
|
28
|
+
# set Occi::Log
|
29
|
+
@logger = get_logger(@options[:log])
|
32
30
|
|
33
|
-
|
34
|
-
|
35
|
-
# is necessary because of OCCI-OS and its
|
36
|
-
# redirect to OS Keystone
|
37
|
-
preauthenticate
|
31
|
+
# check the validity and canonize the endpoint URI
|
32
|
+
@endpoint = get_endpoint_uri(@options[:endpoint])
|
38
33
|
|
39
|
-
|
40
|
-
|
34
|
+
# pass auth options
|
35
|
+
@auth_options = get_auth(@options[:auth])
|
41
36
|
|
42
|
-
|
43
|
-
|
37
|
+
# verify authN before attempting actual
|
38
|
+
# message exchange with the server; this
|
39
|
+
# is necessary because of OCCI-OS and its
|
40
|
+
# redirect to OS Keystone
|
41
|
+
preauthenticate
|
44
42
|
|
45
|
-
|
46
|
-
|
47
|
-
# the auto_connect option during instantiation.
|
48
|
-
#
|
49
|
-
# @example
|
50
|
-
# client.connect # => true
|
51
|
-
#
|
52
|
-
# @param [Boolean] force re-connect on already connected client
|
53
|
-
# @return [Boolean] true on successful connect
|
54
|
-
def connect(force = false)
|
55
|
-
raise "Client already connected!" if @connected && !force
|
56
|
-
@connected = true
|
57
|
-
end
|
43
|
+
# set accepted media types
|
44
|
+
@media_type = get_media_type(@options[:media_type])
|
58
45
|
|
59
|
-
|
60
|
-
|
61
|
-
##############################################################################
|
62
|
-
|
63
|
-
# Retrieves available resources represented by resource locations (URIs).
|
64
|
-
# If no type identifier is specified, all available resource are listed.
|
65
|
-
# Type identifier can be specified in its shortened format (e.g. "compute",
|
66
|
-
# "storage", "network").
|
67
|
-
#
|
68
|
-
# @example
|
69
|
-
# client.list
|
70
|
-
# # => [ "http://localhost:3300/compute/jh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j",
|
71
|
-
# # "http://localhost:3300/network/kh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j",
|
72
|
-
# # "http://localhost:3300/storage/lh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j" ]
|
73
|
-
# client.list "compute"
|
74
|
-
# # => [ "http://localhost:3300/compute/jh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j" ]
|
75
|
-
# client.list "http://schemas.ogf.org/occi/infrastructure#compute"
|
76
|
-
# # => [ "http://localhost:3300/compute/jh425jhj3h413-7dj29d7djd9e3-djh2jh4j4j" ]
|
77
|
-
#
|
78
|
-
# @param [String] resource type identifier or just type name
|
79
|
-
# @return [Array<String>] list of links
|
80
|
-
def list(resource_type_identifier=nil)
|
81
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
82
|
-
end
|
83
|
-
|
84
|
-
# Retrieves descriptions for available resources specified by a type
|
85
|
-
# identifier or resource location. If no type identifier or location
|
86
|
-
# is specified, all available resources in all available resource types
|
87
|
-
# will be described.
|
88
|
-
#
|
89
|
-
# @example
|
90
|
-
# client.describe
|
91
|
-
# # => #<Occi::Core::Resources>
|
92
|
-
# client.describe "compute"
|
93
|
-
# # => #<Occi::Core::Resources>
|
94
|
-
# client.describe "http://schemas.ogf.org/occi/infrastructure#compute"
|
95
|
-
# # => #<Occi::Core::Resources>
|
96
|
-
# client.describe "http://localhost:3300/compute/j5hk1234jk2524-2j3j2k34jjh234-adfaf1234"
|
97
|
-
# # => #<Occi::Core::Resources>
|
98
|
-
#
|
99
|
-
# @param [String] resource type identifier, type name or resource location
|
100
|
-
# @return [Occi::Core::Resources] list of resource descriptions
|
101
|
-
def describe(resource_type_identifier=nil)
|
102
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
103
|
-
end
|
104
|
-
|
105
|
-
# Creates a new resource on the server. Resource must be provided
|
106
|
-
# as an instance of Occi::Core::Entity, e.g. instantiated using
|
107
|
-
# the get_resource method.
|
108
|
-
#
|
109
|
-
# @example
|
110
|
-
# res = client.get_resource "compute"
|
111
|
-
#
|
112
|
-
# res.title = "MyComputeResource1"
|
113
|
-
# res.mixins << client.get_mixin('small', "resource_tpl")
|
114
|
-
# res.mixins << client.get_mixin('debian6', "os_tpl")
|
115
|
-
#
|
116
|
-
# client.create res # => "http://localhost:3300/compute/df7698...f987fa"
|
117
|
-
#
|
118
|
-
# @param [Occi::Core::Entity] resource to be created on the server
|
119
|
-
# @return [String] URI of the new resource
|
120
|
-
def create(entity)
|
121
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
122
|
-
end
|
123
|
-
|
124
|
-
# Deploys a compute resource based on an OVF/OVA descriptor available
|
125
|
-
# on a local file system.
|
126
|
-
#
|
127
|
-
# @example
|
128
|
-
# client.deploy "~/MyVMs/rOcciVM.ovf" # => "http://localhost:3300/compute/343423...42njhdafa"
|
129
|
-
#
|
130
|
-
# @param [String] location of an OVF/OVA file
|
131
|
-
# @return [String] URI of the new resource
|
132
|
-
def deploy(location)
|
133
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
134
|
-
end
|
135
|
-
|
136
|
-
# Deploys a compute resource based on an OVF descriptor available
|
137
|
-
# directly as a String.
|
138
|
-
#
|
139
|
-
# @example
|
140
|
-
# client.deploy_ovf "OVF DESCRIPTOR HERE" # => "http://localhost:3300/compute/343423...42njhdafa"
|
141
|
-
#
|
142
|
-
# @param [String] OVF descriptor (e.g., already read from a file or generated)
|
143
|
-
# @return [String] URI of the new resource
|
144
|
-
def deploy_ovf(descriptor)
|
145
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
146
|
-
end
|
147
|
-
|
148
|
-
# Deploys a compute resource based on an OVA descriptor available
|
149
|
-
# directly as a String.
|
150
|
-
#
|
151
|
-
# @example
|
152
|
-
# client.deploy_ova "OVA DESCRIPTOR HERE" # => "http://localhost:3300/compute/343423...42njhdafa"
|
153
|
-
#
|
154
|
-
# @param [String] OVA descriptor (e.g., already read from a file or generated)
|
155
|
-
# @return [String] URI of the new resource
|
156
|
-
def deploy_ova(descriptor)
|
157
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
158
|
-
end
|
159
|
-
|
160
|
-
# Deletes a resource or all resource of a certain resource type
|
161
|
-
# from the server.
|
162
|
-
#
|
163
|
-
# @example
|
164
|
-
# client.delete "compute" # => true
|
165
|
-
# client.delete "http://schemas.ogf.org/occi/infrastructure#compute" # => true
|
166
|
-
# client.delete "http://localhost:3300/compute/245j42594...98s9df8s9f" # => true
|
167
|
-
#
|
168
|
-
# @param [String] resource type identifier, type name or location
|
169
|
-
# @return [Boolean] status
|
170
|
-
def delete(resource_type_identifier)
|
171
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
172
|
-
end
|
173
|
-
|
174
|
-
# Triggers given action on a specific resource.
|
175
|
-
#
|
176
|
-
# @example
|
177
|
-
# TODO: add examples
|
178
|
-
#
|
179
|
-
# @param [String] resource type or type identifier
|
180
|
-
# @param [Occi::Core::ActionInstance] type of action
|
181
|
-
# @return [String] resource location
|
182
|
-
def trigger(resource_type_identifier, action_instance)
|
183
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
184
|
-
end
|
185
|
-
|
186
|
-
# Refreshes the Occi::Model used inside the client. Useful for
|
187
|
-
# updating the model without creating a new instance or
|
188
|
-
# reconnecting. Saves a lot of time in an interactive mode.
|
189
|
-
#
|
190
|
-
# @example
|
191
|
-
# client.refresh
|
192
|
-
def refresh
|
193
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
194
|
-
end
|
195
|
-
|
196
|
-
##############################################################################
|
197
|
-
######## STUBS END
|
198
|
-
##############################################################################
|
199
|
-
|
200
|
-
# Creates a new resource instance, resource should be specified
|
201
|
-
# by its name or identifier.
|
202
|
-
#
|
203
|
-
# @example
|
204
|
-
# client.get_resource "compute" # => Occi::Core::Resource
|
205
|
-
# client.get_resource "storage" # => Occi::Core::Resource
|
206
|
-
# client.get_resource "http://schemas.ogf.org/occi/infrastructure#network"
|
207
|
-
# # => Occi::Core::Resource
|
208
|
-
#
|
209
|
-
# @param [String] resource name or resource identifier
|
210
|
-
# @return [Occi::Core::Resource] new resource instance
|
211
|
-
def get_resource(resource_type)
|
212
|
-
Occi::Log.debug("Instantiating #{resource_type.inspect}")
|
213
|
-
|
214
|
-
type_id = get_resource_type_identifier(resource_type)
|
215
|
-
raise "Unknown resource type! #{resource_type.inspect}" unless type_id
|
216
|
-
|
217
|
-
new_resource = Occi::Core::Resource.new(type_id)
|
218
|
-
new_resource.model = @model
|
219
|
-
|
220
|
-
new_resource
|
221
|
-
end
|
222
|
-
|
223
|
-
# Retrieves all available category types.
|
224
|
-
#
|
225
|
-
# @example
|
226
|
-
# client.get_category_types # => [ "entity", "resource", "link" ]
|
227
|
-
#
|
228
|
-
# @return [Array<String>] list of available category types in a human-readable format
|
229
|
-
def get_category_types
|
230
|
-
@model.categories.to_a.collect { |category| category.term }
|
231
|
-
end
|
232
|
-
|
233
|
-
# Retrieves all available category type identifiers.
|
234
|
-
#
|
235
|
-
# @example
|
236
|
-
# client.get_category_type_identifiers
|
237
|
-
# # => [ "http://schemas.ogf.org/occi/core#entity",
|
238
|
-
# # "http://schemas.ogf.org/occi/core#resource",
|
239
|
-
# # "http://schemas.ogf.org/occi/core#link" ]
|
240
|
-
#
|
241
|
-
# @return [Array<String>] list of available category type identifiers
|
242
|
-
def get_category_type_identifiers
|
243
|
-
@model.categories.to_a.collect { |category| category.type_identifier }
|
244
|
-
end
|
245
|
-
|
246
|
-
# Retrieves available category type identifier for the given category type.
|
247
|
-
#
|
248
|
-
# @example
|
249
|
-
# client.get_category_type_identifier("compute")
|
250
|
-
# # => 'http://schemas.ogf.org/occi/infrastructure#compute'
|
251
|
-
#
|
252
|
-
# @return [String, nil] category type identifier for the given category type
|
253
|
-
def get_category_type_identifier(type)
|
254
|
-
return type if (type =~ URI::ABS_URI) || (type && type.start_with?('/'))
|
255
|
-
|
256
|
-
cats = @model.categories.to_a.select { |k| k.term == type }
|
257
|
-
tis = cats.collect { |c| c.type_identifier }
|
258
|
-
tis.uniq!
|
259
|
-
|
260
|
-
if tis.length > 1
|
261
|
-
raise Occi::Api::Client::Errors::AmbiguousNameError,
|
262
|
-
"Category type #{type.inspect} is ambiguous, use a type identifier!"
|
263
|
-
end
|
264
|
-
|
265
|
-
tis.first
|
266
|
-
end
|
267
|
-
|
268
|
-
# Retrieves all kind type identifiers related to a given type identifier
|
269
|
-
#
|
270
|
-
# @example
|
271
|
-
# client.get_kind_type_identifiers_related_to 'http://schemas.ogf.org/occi/infrastructure#network'
|
272
|
-
# # => [ "http://schemas.ogf.org/occi/infrastructure#network",
|
273
|
-
# # "http://schemas.ogf.org/occi/infrastructure#ipnetwork" ]
|
274
|
-
#
|
275
|
-
# @param [String] type identifier
|
276
|
-
# @return [Array<String>] list of available kind type identifiers related to
|
277
|
-
# the given type identifier
|
278
|
-
def get_kind_type_identifiers_related_to(type_identifier)
|
279
|
-
Occi::Log.debug("Getting kind type identifiers related to #{type_identifier.inspect}")
|
280
|
-
collection = @model.get(type_identifier)
|
281
|
-
collection.kinds.to_a.collect { |kind| kind.type_identifier }
|
282
|
-
end
|
283
|
-
|
284
|
-
# Retrieves all available kind types.
|
285
|
-
#
|
286
|
-
# @example
|
287
|
-
# client.get_kind_types # => [ "entity", "resource", "link" ]
|
288
|
-
#
|
289
|
-
# @return [Array<String>] list of available kind types in a human-readable format
|
290
|
-
def get_kind_types
|
291
|
-
@model.kinds.to_a.collect { |kind| kind.term }
|
292
|
-
end
|
293
|
-
|
294
|
-
# Retrieves all available kind type identifiers.
|
295
|
-
#
|
296
|
-
# @example
|
297
|
-
# client.get_kind_type_identifiers
|
298
|
-
# # => [ "http://schemas.ogf.org/occi/core#entity",
|
299
|
-
# # "http://schemas.ogf.org/occi/core#resource",
|
300
|
-
# # "http://schemas.ogf.org/occi/core#link" ]
|
301
|
-
#
|
302
|
-
# @return [Array<String>] list of available kind type identifiers
|
303
|
-
def get_kind_type_identifiers
|
304
|
-
@model.kinds.to_a.collect { |kind| kind.type_identifier }
|
305
|
-
end
|
306
|
-
|
307
|
-
# Retrieves available kind type identifier for the given kind type.
|
308
|
-
#
|
309
|
-
# @example
|
310
|
-
# client.get_kind_type_identifier("compute")
|
311
|
-
# # => 'http://schemas.ogf.org/occi/infrastructure#compute'
|
312
|
-
#
|
313
|
-
# @return [String, nil] kind type identifier for the given kind type
|
314
|
-
def get_kind_type_identifier(type)
|
315
|
-
return type if (type =~ URI::ABS_URI) || (type && type.start_with?('/'))
|
316
|
-
|
317
|
-
kinds = @model.kinds.to_a.select { |k| k.term == type }
|
318
|
-
tis = kinds.collect { |k| k.type_identifier }
|
319
|
-
tis.uniq!
|
320
|
-
|
321
|
-
if tis.length > 1
|
322
|
-
raise Occi::Api::Client::Errors::AmbiguousNameError,
|
323
|
-
"Kind type #{type.inspect} is ambiguous, use a type identifier!"
|
324
|
-
end
|
325
|
-
|
326
|
-
tis.first
|
327
|
-
end
|
328
|
-
|
329
|
-
# Retrieves all available entity types.
|
330
|
-
#
|
331
|
-
# @example
|
332
|
-
# client.get_entity_types # => [ "entity", "resource", "link" ]
|
333
|
-
#
|
334
|
-
# @return [Array<String>] list of available entity types in a human-readable format
|
335
|
-
def get_entity_types
|
336
|
-
collection = @model.get(Occi::Core::Entity.kind.type_identifier)
|
337
|
-
collection.kinds.to_a.collect { |kind| kind.term }
|
338
|
-
end
|
339
|
-
|
340
|
-
# Retrieves all available entity type identifiers.
|
341
|
-
#
|
342
|
-
# @example
|
343
|
-
# client.get_kind_type_identifiers
|
344
|
-
# # => [ "http://schemas.ogf.org/occi/core#entity",
|
345
|
-
# # "http://schemas.ogf.org/occi/core#resource",
|
346
|
-
# # "http://schemas.ogf.org/occi/core#link" ]
|
347
|
-
#
|
348
|
-
# @return [Array<String>] list of available entity types in a OCCI ID format
|
349
|
-
def get_entity_type_identifiers
|
350
|
-
get_kind_type_identifiers_related_to Occi::Core::Entity.kind.type_identifier
|
351
|
-
end
|
352
|
-
|
353
|
-
# Retrieves available entity type identifier for the given entity type.
|
354
|
-
#
|
355
|
-
# @example
|
356
|
-
# client.get_entity_type_identifier("compute")
|
357
|
-
# # => 'http://schemas.ogf.org/occi/infrastructure#compute'
|
358
|
-
#
|
359
|
-
# @return [String, nil] entity type identifier for the given entity type
|
360
|
-
def get_entity_type_identifier(type)
|
361
|
-
return type if (type =~ URI::ABS_URI) || (type && type.start_with?('/'))
|
362
|
-
|
363
|
-
collection = @model.get(Occi::Core::Entity.kind.type_identifier)
|
364
|
-
e_kinds = collection.kinds.to_a.select { |e| e.term == type }
|
365
|
-
tis = e_kinds.collect { |e| e.type_identifier }
|
366
|
-
tis.uniq!
|
367
|
-
|
368
|
-
if tis.length > 1
|
369
|
-
raise Occi::Api::Client::Errors::AmbiguousNameError,
|
370
|
-
"Entity type #{type.inspect} is ambiguous, use a type identifier!"
|
371
|
-
end
|
372
|
-
|
373
|
-
tis.first
|
374
|
-
end
|
375
|
-
|
376
|
-
# Retrieves all available resource types.
|
377
|
-
#
|
378
|
-
# @example
|
379
|
-
# client.get_resource_types # => [ "compute", "storage", "network" ]
|
380
|
-
#
|
381
|
-
# @return [Array<String>] list of available resource types in a human-readable format
|
382
|
-
def get_resource_types
|
383
|
-
collection = @model.get(Occi::Core::Resource.kind.type_identifier)
|
384
|
-
collection.kinds.to_a.collect { |kind| kind.term }
|
385
|
-
end
|
386
|
-
|
387
|
-
# Retrieves all available resource type identifiers.
|
388
|
-
#
|
389
|
-
# @example
|
390
|
-
# client.get_resource_type_identifiers
|
391
|
-
# # => [ "http://schemas.ogf.org/occi/infrastructure#compute",
|
392
|
-
# # "http://schemas.ogf.org/occi/infrastructure#storage",
|
393
|
-
# # "http://schemas.ogf.org/occi/infrastructure#network" ]
|
394
|
-
#
|
395
|
-
# @return [Array<String>] list of available resource types in a Occi ID format
|
396
|
-
def get_resource_type_identifiers
|
397
|
-
get_kind_type_identifiers_related_to Occi::Core::Resource.kind.type_identifier
|
398
|
-
end
|
399
|
-
|
400
|
-
# Retrieves available resource type identifier for the given resource type.
|
401
|
-
#
|
402
|
-
# @example
|
403
|
-
# client.get_resource_type_identifier("compute")
|
404
|
-
# # => 'http://schemas.ogf.org/occi/infrastructure#compute'
|
405
|
-
#
|
406
|
-
# @return [String, nil] resource type identifier for the given resource type
|
407
|
-
def get_resource_type_identifier(type)
|
408
|
-
return type if (type =~ URI::ABS_URI) || (type && type.start_with?('/'))
|
409
|
-
|
410
|
-
collection = @model.get(Occi::Core::Resource.kind.type_identifier)
|
411
|
-
r_kinds = collection.kinds.to_a.select { |r| r.term == type }
|
412
|
-
tis = r_kinds.collect { |r| r.type_identifier }
|
413
|
-
tis.uniq!
|
414
|
-
|
415
|
-
if tis.length > 1
|
416
|
-
raise Occi::Api::Client::Errors::AmbiguousNameError,
|
417
|
-
"Resource type #{type.inspect} is ambiguous, use a type identifier!"
|
418
|
-
end
|
419
|
-
|
420
|
-
tis.first
|
421
|
-
end
|
422
|
-
|
423
|
-
# Retrieves all available link types.
|
424
|
-
#
|
425
|
-
# @example
|
426
|
-
# client.get_link_types # => [ "storagelink", "networkinterface" ]
|
427
|
-
#
|
428
|
-
# @return [Array<String>] list of available link types in a human-readable format
|
429
|
-
def get_link_types
|
430
|
-
collection = @model.get(Occi::Core::Link.kind.type_identifier)
|
431
|
-
collection.kinds.to_a.collect { |kind| kind.term }
|
432
|
-
end
|
433
|
-
|
434
|
-
# Retrieves all available link type identifiers.
|
435
|
-
#
|
436
|
-
# @example
|
437
|
-
# client.get_link_type_identifiers
|
438
|
-
# # => [ "http://schemas.ogf.org/occi/infrastructure#storagelink",
|
439
|
-
# # "http://schemas.ogf.org/occi/infrastructure#networkinterface" ]
|
440
|
-
#
|
441
|
-
# @return [Array<String>] list of available link types in a OCCI ID format
|
442
|
-
def get_link_type_identifiers
|
443
|
-
get_kind_type_identifiers_related_to Occi::Core::Link.kind.type_identifier
|
444
|
-
end
|
445
|
-
|
446
|
-
# Retrieves available link type identifier for the given link type.
|
447
|
-
#
|
448
|
-
# @example
|
449
|
-
# client.get_link_type_identifier("storagelink")
|
450
|
-
# # => 'http://schemas.ogf.org/occi/infrastructure#storagelink'
|
451
|
-
#
|
452
|
-
# @return [String, nil] link type identifier for the given link type
|
453
|
-
def get_link_type_identifier(type)
|
454
|
-
return type if (type =~ URI::ABS_URI) || (type && type.start_with?('/'))
|
455
|
-
|
456
|
-
collection = @model.get(Occi::Core::Link.kind.type_identifier)
|
457
|
-
l_kinds = collection.kinds.to_a.select { |r| r.term == type }
|
458
|
-
tis = l_kinds.collect { |r| r.type_identifier }
|
459
|
-
tis.uniq!
|
460
|
-
|
461
|
-
if tis.length > 1
|
462
|
-
raise Occi::Api::Client::Errors::AmbiguousNameError,
|
463
|
-
"Link type #{type.inspect} is ambiguous, use a type identifier!"
|
464
|
-
end
|
465
|
-
|
466
|
-
tis.first
|
467
|
-
end
|
468
|
-
|
469
|
-
# Looks up a mixin using its name and, optionally, a type as well.
|
470
|
-
# Will return mixin's full location (a link) or a description.
|
471
|
-
#
|
472
|
-
# @example
|
473
|
-
# client.get_mixin "debian6"
|
474
|
-
# # => "http://my.occi.service/occi/infrastructure/os_tpl#debian6"
|
475
|
-
# client.get_mixin "debian6", "os_tpl", true
|
476
|
-
# # => #<Occi::Core::Mixin>
|
477
|
-
# client.get_mixin "large", "resource_tpl"
|
478
|
-
# # => "http://my.occi.service/occi/infrastructure/resource_tpl#large"
|
479
|
-
# client.get_mixin "debian6", "resource_tpl" # => nil
|
480
|
-
#
|
481
|
-
# @param [String] name of the mixin
|
482
|
-
# @param [String] type of the mixin
|
483
|
-
# @param [Boolean] should we describe the mixin or return its link?
|
484
|
-
# @return [String, Occi::Core::Mixin, nil] link, mixin description or nothing found
|
485
|
-
def get_mixin(name, type = nil, describe = false)
|
486
|
-
# TODO: mixin fix
|
487
|
-
Occi::Log.debug("Looking for mixin #{name} + #{type} + #{describe}")
|
488
|
-
|
489
|
-
# TODO: extend this code to support multiple matches and regex filters
|
490
|
-
# should we look for links or descriptions?
|
491
|
-
describe ? describe_mixin(name, type) : list_mixin(name, type)
|
492
|
-
end
|
493
|
-
|
494
|
-
# Looks up a mixin using its name and, optionally, a type as well.
|
495
|
-
# Will return mixin's full description.
|
496
|
-
#
|
497
|
-
# @example
|
498
|
-
# client.describe_mixin "debian6"
|
499
|
-
# # => #<Occi::Core::Mixin>
|
500
|
-
# client.describe_mixin "debian6", "os_tpl"
|
501
|
-
# # => #<Occi::Core::Mixin>
|
502
|
-
# client.describe_mixin "large", "resource_tpl"
|
503
|
-
# # => #<Occi::Core::Mixin>
|
504
|
-
# client.describe_mixin "debian6", "resource_tpl" # => nil
|
505
|
-
#
|
506
|
-
# @param [String] name of the mixin
|
507
|
-
# @param [String] type of the mixin
|
508
|
-
# @return [Occi::Core::Mixin, nil] mixin description or nothing found
|
509
|
-
def describe_mixin(name, type = nil)
|
510
|
-
mixins = get_mixins(type)
|
511
|
-
|
512
|
-
mixins = mixins.to_a.select { |m| m.term == name }
|
513
|
-
mixins.any? ? mixins.first : nil
|
514
|
-
end
|
515
|
-
|
516
|
-
# Looks up a mixin with a specific type, will return
|
517
|
-
# mixin's full description.
|
518
|
-
#
|
519
|
-
# @param [String] name of the mixin
|
520
|
-
# @param [String] type of the mixin
|
521
|
-
# @return [Occi::Core::Mixin] mixin description
|
522
|
-
def describe_mixin_w_type(name, type)
|
523
|
-
describe_mixin(name, type)
|
524
|
-
end
|
525
|
-
|
526
|
-
# Looks up a mixin in all available mixin types, will
|
527
|
-
# return mixin's full description. Returns always the
|
528
|
-
# first match found, search will start in os_tpl.
|
529
|
-
#
|
530
|
-
# @param [String] name of the mixin
|
531
|
-
# @return [Occi::Core::Mixin] mixin description
|
532
|
-
def describe_mixin_wo_type(name)
|
533
|
-
describe_mixin(name, nil)
|
534
|
-
end
|
535
|
-
|
536
|
-
# Looks up a mixin using its name and, optionally, a type as well.
|
537
|
-
# Will return mixin's full location.
|
538
|
-
#
|
539
|
-
# @example
|
540
|
-
# client.list_mixin "debian6"
|
541
|
-
# # => "http://my.occi.service/occi/infrastructure/os_tpl#debian6"
|
542
|
-
# client.list_mixin "debian6", "os_tpl"
|
543
|
-
# # => "http://my.occi.service/occi/infrastructure/os_tpl#debian6"
|
544
|
-
# client.list_mixin "large", "resource_tpl"
|
545
|
-
# # => "http://my.occi.service/occi/infrastructure/resource_tpl#large"
|
546
|
-
# client.list_mixin "debian6", "resource_tpl" # => nil
|
547
|
-
#
|
548
|
-
# @param [String] name of the mixin
|
549
|
-
# @param [String] type of the mixin
|
550
|
-
# @return [String, nil] link or nothing found
|
551
|
-
def list_mixin(name, type = nil)
|
552
|
-
mixin = describe_mixin(name, type)
|
553
|
-
mixin ? mixin.type_identifier : nil
|
554
|
-
end
|
555
|
-
|
556
|
-
# Retrieves available mixins of a specified type or all available
|
557
|
-
# mixins if the type wasn't specified. Mixins are returned in the
|
558
|
-
# form of mixin instances.
|
559
|
-
#
|
560
|
-
# @example
|
561
|
-
# client.get_mixins
|
562
|
-
# # => #<Occi::Core::Mixins>
|
563
|
-
# client.get_mixins "os_tpl"
|
564
|
-
# # => #<Occi::Core::Mixins>
|
565
|
-
# client.get_mixins "resource_tpl"
|
566
|
-
# # => #<Occi::Core::Mixins>
|
567
|
-
#
|
568
|
-
# @param [String] type of mixins
|
569
|
-
# @return [Occi::Core::Mixins] collection of available mixins
|
570
|
-
def get_mixins(type = nil)
|
571
|
-
unless type.blank?
|
572
|
-
type_id = get_mixin_type_identifier(type)
|
573
|
-
unless type_id
|
574
|
-
raise ArgumentError,
|
575
|
-
"There is no such mixin type registered in the model! #{type.inspect}"
|
576
|
-
end
|
577
|
-
|
578
|
-
mixins = @model.mixins.to_a.select { |m| m.related_to?(type_id) }
|
579
|
-
|
580
|
-
# drop the type mixin itself
|
581
|
-
mixins.delete_if { |m| m.type_identifier == type_id }
|
582
|
-
else
|
583
|
-
# we did not get a type, return all mixins
|
584
|
-
mixins = Occi::Core::Mixins.new(@model.mixins)
|
585
|
-
end
|
586
|
-
|
587
|
-
unless mixins.kind_of? Occi::Core::Mixins
|
588
|
-
col = Occi::Core::Mixins.new
|
589
|
-
mixins.each { |m| col << m }
|
590
|
-
else
|
591
|
-
col = mixins
|
592
|
-
end
|
593
|
-
|
594
|
-
col
|
595
|
-
end
|
596
|
-
|
597
|
-
# Retrieves available mixins of a specified type or all available
|
598
|
-
# mixins if the type wasn't specified. Mixins are returned in the
|
599
|
-
# form of mixin identifiers.
|
600
|
-
#
|
601
|
-
# @example
|
602
|
-
# client.list_mixins
|
603
|
-
# # => #<Array<String>>
|
604
|
-
# client.list_mixins "os_tpl"
|
605
|
-
# # => #<Array<String>>
|
606
|
-
# client.list_mixins "resource_tpl"
|
607
|
-
# # => #<Array<String>>
|
608
|
-
#
|
609
|
-
# @param [String] type of mixins
|
610
|
-
# @return [Array<String>] collection of available mixin identifiers
|
611
|
-
def list_mixins(type = nil)
|
612
|
-
mixins = get_mixins(type)
|
613
|
-
mixins.to_a.collect { |m| m.type_identifier }
|
614
|
-
end
|
615
|
-
|
616
|
-
# Retrieves available mixin types. Mixin types are presented
|
617
|
-
# in a shortened format (i.e. not as type identifiers).
|
618
|
-
#
|
619
|
-
# @example
|
620
|
-
# client.get_mixin_types # => [ "os_tpl", "resource_tpl" ]
|
621
|
-
#
|
622
|
-
# @return [Array<String>] list of available mixin types
|
623
|
-
def get_mixin_types
|
624
|
-
get_mixins.to_a.collect { |m| m.term }
|
625
|
-
end
|
626
|
-
|
627
|
-
# Retrieves available mixin type identifiers.
|
628
|
-
#
|
629
|
-
# @example
|
630
|
-
# client.get_mixin_type_identifiers
|
631
|
-
# # => ['http://schemas.ogf.org/occi/infrastructure#os_tpl',
|
632
|
-
# # 'http://schemas.ogf.org/occi/infrastructure#resource_tpl']
|
633
|
-
#
|
634
|
-
# @return [Array<String>] list of available mixin type identifiers
|
635
|
-
def get_mixin_type_identifiers
|
636
|
-
list_mixins(nil)
|
637
|
-
end
|
638
|
-
|
639
|
-
# Retrieves available mixin type identifier for the given mixin type.
|
640
|
-
#
|
641
|
-
# @example
|
642
|
-
# client.get_mixin_type_identifier("os_tpl")
|
643
|
-
# # => 'http://schemas.ogf.org/occi/infrastructure#os_tpl'
|
644
|
-
#
|
645
|
-
# @return [String, nil] mixin type identifier for the given mixin type
|
646
|
-
def get_mixin_type_identifier(type)
|
647
|
-
return type if (type =~ URI::ABS_URI) || (type && type.start_with?('/'))
|
648
|
-
|
649
|
-
mixins = @model.mixins.to_a.select { |m| m.term == type }
|
650
|
-
tis = mixins.collect { |m| m.type_identifier }
|
651
|
-
tis.uniq!
|
652
|
-
|
653
|
-
if tis.length > 1
|
654
|
-
raise Occi::Api::Client::Errors::AmbiguousNameError,
|
655
|
-
"Mixin type #{type.inspect} is ambiguous, use a type identifier!"
|
656
|
-
end
|
657
|
-
|
658
|
-
tis.first
|
659
|
-
end
|
660
|
-
|
661
|
-
# Retrieves available os_tpls from the model.
|
662
|
-
#
|
663
|
-
# @example
|
664
|
-
# get_os_templates # => #<Occi::Core::Mixins>
|
665
|
-
#
|
666
|
-
# @return [Occi::Core::Mixins] collection containing all registered OS templates
|
667
|
-
def get_os_templates
|
668
|
-
get_mixins Occi::Infrastructure::OsTpl.mixin.type_identifier
|
669
|
-
end
|
670
|
-
alias_method :get_os_tpls, :get_os_templates
|
671
|
-
|
672
|
-
# Retrieves available resource_tpls from the model.
|
673
|
-
#
|
674
|
-
# @example
|
675
|
-
# get_resource_templates # => #<Occi::Core::Mixins>
|
676
|
-
#
|
677
|
-
# @return [Occi::Core::Mixins] collection containing all registered resource templates
|
678
|
-
def get_resource_templates
|
679
|
-
get_mixins Occi::Infrastructure::ResourceTpl.mixin.type_identifier
|
680
|
-
end
|
681
|
-
alias_method :get_resource_tpls, :get_resource_templates
|
682
|
-
|
683
|
-
# Returns the path for a given kind type identifier
|
684
|
-
#
|
685
|
-
# @example
|
686
|
-
# path_for_kind_type_identifier "http://schemas.ogf.org/occi/infrastructure#compute"
|
687
|
-
# # => "/compute/"
|
688
|
-
# path_for_kind_type_identifier "http://localhost:3300/compute/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
|
689
|
-
# # => "/compute/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
|
690
|
-
#
|
691
|
-
# @param [String] kind type identifier
|
692
|
-
# @return [String]
|
693
|
-
def path_for_kind_type_identifier(kind_type_identifier)
|
694
|
-
raise ArgumentError,
|
695
|
-
"Kind type identifier is a required argument!" if kind_type_identifier.blank?
|
696
|
-
|
697
|
-
if kind_type_identifier.start_with?(@endpoint.to_s) || kind_type_identifier.start_with?('/')
|
698
|
-
#we got an instance link
|
699
|
-
return sanitize_instance_link(kind_type_identifier)
|
700
|
-
end
|
701
|
-
|
702
|
-
kind_type_id = get_kind_type_identifier(kind_type_identifier)
|
703
|
-
unless kind_type_id
|
704
|
-
raise ArgumentError,
|
705
|
-
"There is no such kind type registered in the model! #{kind_type_identifier.inspect}"
|
706
|
-
end
|
707
|
-
|
708
|
-
kinds = @model.kinds.select { |kind| kind.type_identifier == kind_type_id }
|
709
|
-
path_for_instance(kinds.first)
|
710
|
-
end
|
711
|
-
|
712
|
-
# Returns the path for a given instance, instances not providing
|
713
|
-
# path information will raise an exception.
|
714
|
-
#
|
715
|
-
# @example
|
716
|
-
# path_for_instance Occi::Infrastructure::Network.new
|
717
|
-
# # => "/network/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
|
718
|
-
# path_for_instance Occi::Infrastructure::Compute.new
|
719
|
-
# # => "/compute/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
|
720
|
-
# path_for_instance Occi::Core::Mixin.new
|
721
|
-
# # => "/mixin/my_mixin/"
|
722
|
-
# path_for_instance Occi::Infrastructure::Storagelink.new
|
723
|
-
# # => "/link/storagelink/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
|
724
|
-
#
|
725
|
-
# @param [Object] instance
|
726
|
-
# @return [String] path for the given instance
|
727
|
-
def path_for_instance(instance)
|
728
|
-
unless instance.respond_to?(:location)
|
729
|
-
raise Occi::Api::Client::Errors::TypeMismatchError,
|
730
|
-
"Expected an instance responding to #location, " \
|
731
|
-
"got #{instance.class.name.inspect}"
|
732
|
-
end
|
733
|
-
|
734
|
-
if instance.location.blank?
|
735
|
-
raise Occi::Api::Client::Errors::LocationError,
|
736
|
-
"Instance of #{instance.class.name.inspect} has " \
|
737
|
-
"an empty location"
|
738
|
-
end
|
739
|
-
|
740
|
-
instance.location
|
741
|
-
end
|
742
|
-
|
743
|
-
# Extracts path from an instance link. It will remove the leading @endpoint
|
744
|
-
# and replace it with a slash.
|
745
|
-
#
|
746
|
-
# @example
|
747
|
-
# sanitize_instance_link "http://localhost:3300/compute/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
|
748
|
-
# # => "/compute/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
|
749
|
-
# sanitize_instance_link "/compute/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
|
750
|
-
# # => "/compute/35ad4f45gsf-gsfg524s6gsfg-sfgsf4gsfg"
|
751
|
-
#
|
752
|
-
# @param [String] string containing the full instance link
|
753
|
-
# @return [String] extracted path, with a leading slash
|
754
|
-
def sanitize_instance_link(instance_link)
|
755
|
-
# everything starting with '/' is considered to be a resource path
|
756
|
-
return instance_link if instance_link.start_with? '/'
|
757
|
-
|
758
|
-
unless instance_link.start_with?(@endpoint.to_s)
|
759
|
-
raise ArgumentError, "Resource link #{instance_link.inspect} is not valid!"
|
760
|
-
end
|
761
|
-
|
762
|
-
URI(instance_link).request_uri
|
763
|
-
end
|
764
|
-
|
765
|
-
protected
|
766
|
-
|
767
|
-
##############################################################################
|
768
|
-
######## STUBS START
|
769
|
-
##############################################################################
|
770
|
-
|
771
|
-
# Sets auth method and appropriate httparty attributes. Supported auth methods
|
772
|
-
# are: ["basic", "digest", "x509", "none"]
|
773
|
-
#
|
774
|
-
# @example
|
775
|
-
# get_auth { :type => "none" }
|
776
|
-
# get_auth { :type => "basic", :username => "123", :password => "321" }
|
777
|
-
# get_auth { :type => "digest", :username => "123", :password => "321" }
|
778
|
-
# get_auth { :type => "x509", :user_cert => "~/cert.pem",
|
779
|
-
# :user_cert_password => "321", :ca_path => nil }
|
780
|
-
#
|
781
|
-
# @param [Hash] authentication options
|
782
|
-
# @param [Boolean] allow fallback-only options
|
783
|
-
# @return [Hash] transformed hash with authN information
|
784
|
-
def get_auth(auth_options, fallback = false)
|
785
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
786
|
-
end
|
787
|
-
|
788
|
-
# Attempts to establish a preliminary connection with the server
|
789
|
-
# to verify provided credentials and perform fallback authN
|
790
|
-
# if necessary. Has to be invoked after @auth_options have been set.
|
791
|
-
def preauthenticate
|
792
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
793
|
-
end
|
794
|
-
|
795
|
-
# Sets media type. Will choose either application/occi+json or text/plain
|
796
|
-
# based on the formats supported by the server.
|
797
|
-
#
|
798
|
-
# @example
|
799
|
-
# get_media_type # => 'application/occi+json'
|
800
|
-
#
|
801
|
-
# @return [String] chosen media type
|
802
|
-
def get_media_type(force_type = nil)
|
803
|
-
raise Occi::Api::Client::Errors::NotImplementedError, "#{__method__} is just a stub!"
|
804
|
-
end
|
805
|
-
|
806
|
-
##############################################################################
|
807
|
-
######## STUBS END
|
808
|
-
##############################################################################
|
46
|
+
@connected = false
|
47
|
+
end
|
809
48
|
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
logger = log_options[:logger]
|
824
|
-
end
|
49
|
+
# Issues necessary connecting operations on connection-oriented
|
50
|
+
# clients. Stateless clients (such as ClientHttp) should use
|
51
|
+
# the auto_connect option during instantiation.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# client.connect # => true
|
55
|
+
#
|
56
|
+
# @param [Boolean] force re-connect on already connected client
|
57
|
+
# @return [Boolean] true on successful connect
|
58
|
+
def connect(force = false)
|
59
|
+
raise "Client already connected!" if @connected && !force
|
60
|
+
@connected = true
|
61
|
+
end
|
825
62
|
|
826
|
-
|
827
|
-
|
63
|
+
# include stuff
|
64
|
+
include Occi::Api::Client::Base::Stubs
|
828
65
|
|
829
|
-
|
830
|
-
|
831
|
-
#
|
832
|
-
# @example
|
833
|
-
# get_endpoint_uri "http://localhost:3300" # => #<URI::*>
|
834
|
-
#
|
835
|
-
# @param [String] endpoint URI in a non-canonical string
|
836
|
-
# @return [URI] canonical endpoint URI
|
837
|
-
def get_endpoint_uri(endpoint)
|
838
|
-
unless endpoint =~ URI::ABS_URI
|
839
|
-
raise "Endpoint not a valid absolute URI! #{endpoint.inspect}"
|
840
|
-
end
|
66
|
+
# include category-related stuff
|
67
|
+
include Occi::Api::Client::Base::CategoryMethods
|
841
68
|
|
842
|
-
|
843
|
-
|
844
|
-
endpoint.path = endpoint.path.gsub(/\/+/, '/').chomp('/')
|
69
|
+
# include kind-related stuff
|
70
|
+
include Occi::Api::Client::Base::KindMethods
|
845
71
|
|
846
|
-
|
847
|
-
|
72
|
+
# include entity-related stuff
|
73
|
+
include Occi::Api::Client::Base::EntityMethods
|
848
74
|
|
849
|
-
|
850
|
-
|
851
|
-
# @example
|
852
|
-
# model_collection = get('/-/')
|
853
|
-
# get_model model_collection # => #<Occi::Model>
|
854
|
-
#
|
855
|
-
# @param [Occi::Collection] parsed representation of server's model
|
856
|
-
# @return [Occi::Model] Model instance
|
857
|
-
def get_model(model_collection)
|
858
|
-
# build model
|
859
|
-
Occi::Model.new(model_collection)
|
860
|
-
end
|
75
|
+
# include mixin-related stuff
|
76
|
+
include Occi::Api::Client::Base::MixinMethods
|
861
77
|
|
862
|
-
|
863
|
-
|
864
|
-
#
|
865
|
-
# @return [Array] array of os_tpl mixin identifiers
|
866
|
-
def get_os_tpl_mixins_ary
|
867
|
-
mixins = get_os_tpls
|
868
|
-
mixins.to_a.collect { |m| m.type_identifier }
|
869
|
-
end
|
78
|
+
# include helpers
|
79
|
+
include Occi::Api::Client::Base::Helpers
|
870
80
|
|
871
|
-
|
872
|
-
# in an array.
|
873
|
-
#
|
874
|
-
# @return [Array] array of resource_tpl mixin identifiers
|
875
|
-
def get_resource_tpl_mixins_ary
|
876
|
-
mixins = get_resource_tpls
|
877
|
-
mixins.to_a.collect { |m| m.type_identifier }
|
878
|
-
end
|
81
|
+
protected
|
879
82
|
|
880
|
-
|
83
|
+
# include protected stuff
|
84
|
+
include Occi::Api::Client::Base::ProtectedStubs
|
85
|
+
include Occi::Api::Client::Base::ProtectedHelpers
|
881
86
|
|
882
|
-
end
|
883
87
|
end
|
88
|
+
|
884
89
|
end
|