vra-restapi 1.5.2
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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rubocop.yml +14 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +202 -0
- data/README.md +220 -0
- data/Rakefile +8 -0
- data/lib/vra-restapi.rb +29 -0
- data/lib/vra-restapi/catalog.rb +41 -0
- data/lib/vra-restapi/catalog_item.rb +81 -0
- data/lib/vra-restapi/catalog_request.rb +112 -0
- data/lib/vra-restapi/client.rb +223 -0
- data/lib/vra-restapi/exceptions.rb +61 -0
- data/lib/vra-restapi/request.rb +97 -0
- data/lib/vra-restapi/request_parameters.rb +67 -0
- data/lib/vra-restapi/requests.rb +40 -0
- data/lib/vra-restapi/resource.rb +234 -0
- data/lib/vra-restapi/resources.rb +42 -0
- data/lib/vra-restapi/version.rb +21 -0
- data/spec/catalog_item_spec.rb +98 -0
- data/spec/catalog_request_spec.rb +124 -0
- data/spec/catalog_spec.rb +124 -0
- data/spec/client_spec.rb +481 -0
- data/spec/fixtures/resource/non_vm_resource.json +18 -0
- data/spec/fixtures/resource/vm_resource.json +251 -0
- data/spec/fixtures/resource/vm_resource_no_operations.json +101 -0
- data/spec/request_spec.rb +145 -0
- data/spec/requests_spec.rb +59 -0
- data/spec/resource_spec.rb +394 -0
- data/spec/resources_spec.rb +59 -0
- data/spec/spec_helper.rb +22 -0
- data/vra-restapi.gemspec +30 -0
- metadata +201 -0
data/Rakefile
ADDED
data/lib/vra-restapi.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Chef Partner Engineering (<partnereng@chef.io>)
|
3
|
+
# Copyright:: Copyright (c) 2015 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'vra-restapi/catalog'
|
20
|
+
require 'vra-restapi/catalog_item'
|
21
|
+
require 'vra-restapi/catalog_request'
|
22
|
+
require 'vra-restapi/client'
|
23
|
+
require 'vra-restapi/exceptions'
|
24
|
+
require 'vra-restapi/request'
|
25
|
+
require 'vra-restapi/request_parameters'
|
26
|
+
require 'vra-restapi/requests'
|
27
|
+
require 'vra-restapi/resource'
|
28
|
+
require 'vra-restapi/resources'
|
29
|
+
require 'vra-restapi/version'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Chef Partner Engineering (<partnereng@chef.io>)
|
3
|
+
# Copyright:: Copyright (c) 2015 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
module Vra
|
20
|
+
class Catalog
|
21
|
+
attr_reader :client
|
22
|
+
|
23
|
+
def initialize(client)
|
24
|
+
@client = client
|
25
|
+
end
|
26
|
+
|
27
|
+
def all_items
|
28
|
+
client.http_get_paginated_array!('/catalog-service/api/consumer/catalogItems')
|
29
|
+
.map! { |x| Vra::CatalogItem.new(client, data: x) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def entitled_items
|
33
|
+
client.http_get_paginated_array!('/catalog-service/api/consumer/entitledCatalogItems')
|
34
|
+
.map! { |x| Vra::CatalogItem.new(client, data: x['catalogItem']) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def request(*args)
|
38
|
+
Vra::CatalogRequest.new(@client, *args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Chef Partner Engineering (<partnereng@chef.io>)
|
3
|
+
# Copyright:: Copyright (c) 2015 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require 'json'
|
19
|
+
|
20
|
+
module Vra
|
21
|
+
class CatalogItem
|
22
|
+
attr_reader :id, :client
|
23
|
+
def initialize(client, opts)
|
24
|
+
@client = client
|
25
|
+
@id = opts[:id]
|
26
|
+
@catalog_item_data = opts[:data]
|
27
|
+
|
28
|
+
if @id.nil? && @catalog_item_data.nil?
|
29
|
+
raise ArgumentError, 'must supply an id or a catalog item data hash'
|
30
|
+
end
|
31
|
+
|
32
|
+
if ! @id.nil? && ! @catalog_item_data.nil?
|
33
|
+
raise ArgumentError, 'must supply an id OR a catalog item data hash, not both'
|
34
|
+
end
|
35
|
+
|
36
|
+
if @catalog_item_data.nil?
|
37
|
+
fetch_catalog_item
|
38
|
+
else
|
39
|
+
@id = @catalog_item_data['id']
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def fetch_catalog_item
|
44
|
+
@catalog_item_data = JSON.parse(client.http_get!("/catalog-service/api/consumer/catalogItems/#{id}"))
|
45
|
+
rescue Vra::Exception::HTTPNotFound
|
46
|
+
raise Vra::Exception::NotFound, "catalog ID #{id} does not exist"
|
47
|
+
end
|
48
|
+
|
49
|
+
def name
|
50
|
+
@catalog_item_data['name']
|
51
|
+
end
|
52
|
+
|
53
|
+
def description
|
54
|
+
@catalog_item_data['description']
|
55
|
+
end
|
56
|
+
|
57
|
+
def status
|
58
|
+
@catalog_item_data['status']
|
59
|
+
end
|
60
|
+
|
61
|
+
def tenant_id
|
62
|
+
@catalog_item_data['organization']['tenantRef']
|
63
|
+
end
|
64
|
+
|
65
|
+
def tenant_name
|
66
|
+
@catalog_item_data['organization']['tenantLabel']
|
67
|
+
end
|
68
|
+
|
69
|
+
def subtenant_id
|
70
|
+
@catalog_item_data['organization']['subtenantRef']
|
71
|
+
end
|
72
|
+
|
73
|
+
def subtenant_name
|
74
|
+
@catalog_item_data['organization']['subtenantLabel']
|
75
|
+
end
|
76
|
+
|
77
|
+
def blueprint_id
|
78
|
+
@catalog_item_data['providerBinding']['bindingId']
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Chef Partner Engineering (<partnereng@chef.io>)
|
3
|
+
# Copyright:: Copyright (c) 2015 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
module Vra
|
20
|
+
class CatalogRequest
|
21
|
+
attr_reader :catalog_id, :catalog_item, :client, :custom_fields
|
22
|
+
attr_writer :subtenant_id
|
23
|
+
attr_accessor :cpus, :memory, :requested_for, :lease_days, :notes
|
24
|
+
|
25
|
+
def initialize(client, catalog_id, opts={})
|
26
|
+
@client = client
|
27
|
+
@catalog_id = catalog_id
|
28
|
+
@cpus = opts[:cpus]
|
29
|
+
@memory = opts[:memory]
|
30
|
+
@requested_for = opts[:requested_for]
|
31
|
+
@lease_days = opts[:lease_days]
|
32
|
+
@notes = opts[:notes]
|
33
|
+
@subtenant_id = opts[:subtenant_id]
|
34
|
+
@additional_params = Vra::RequestParameters.new
|
35
|
+
|
36
|
+
@catalog_item = Vra::CatalogItem.new(client, id: catalog_id)
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_parameter(key, type, value)
|
40
|
+
@additional_params.set(key, type, value)
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete_parameter(key)
|
44
|
+
@additional_params.delete(key)
|
45
|
+
end
|
46
|
+
|
47
|
+
def parameters
|
48
|
+
@additional_params.all_entries
|
49
|
+
end
|
50
|
+
|
51
|
+
def subtenant_id
|
52
|
+
@subtenant_id || catalog_item.subtenant_id
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_params!
|
56
|
+
missing_params = []
|
57
|
+
[ :catalog_id, :cpus, :memory, :requested_for, :subtenant_id ].each do |param|
|
58
|
+
missing_params << param.to_s if send(param).nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
raise ArgumentError, "Unable to submit request, required param(s) missing => #{missing_params.join(', ')}" unless missing_params.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def request_payload
|
65
|
+
payload = {
|
66
|
+
'@type' => 'CatalogItemRequest',
|
67
|
+
'catalogItemRef' => {
|
68
|
+
'id' => @catalog_id
|
69
|
+
},
|
70
|
+
'organization' => {
|
71
|
+
'tenantRef' => catalog_item.tenant_id,
|
72
|
+
'subtenantRef' => subtenant_id
|
73
|
+
},
|
74
|
+
'requestedFor' => @requested_for,
|
75
|
+
'state' => 'SUBMITTED',
|
76
|
+
'requestNumber' => 0,
|
77
|
+
'requestData' => {
|
78
|
+
'entries' => [
|
79
|
+
Vra::RequestParameter.new('provider-blueprintId', 'string', catalog_item.blueprint_id).to_h,
|
80
|
+
Vra::RequestParameter.new('provider-provisioningGroupId', 'string', subtenant_id).to_h,
|
81
|
+
Vra::RequestParameter.new('requestedFor', 'string', @requested_for).to_h,
|
82
|
+
Vra::RequestParameter.new('provider-VirtualMachine.CPU.Count', 'integer', @cpus).to_h,
|
83
|
+
Vra::RequestParameter.new('provider-VirtualMachine.Memory.Size', 'integer', @memory).to_h,
|
84
|
+
Vra::RequestParameter.new('provider-VirtualMachine.LeaseDays', 'integer', @lease_days).to_h,
|
85
|
+
Vra::RequestParameter.new('provider-__Notes', 'string', @notes).to_h
|
86
|
+
]
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
parameters.each do |entry|
|
91
|
+
payload['requestData']['entries'] << entry.to_h
|
92
|
+
end
|
93
|
+
|
94
|
+
payload
|
95
|
+
end
|
96
|
+
|
97
|
+
def submit
|
98
|
+
validate_params!
|
99
|
+
|
100
|
+
begin
|
101
|
+
response = client.http_post('/catalog-service/api/consumer/requests', request_payload.to_json)
|
102
|
+
rescue Vra::Exception::HTTPError => e
|
103
|
+
raise Vra::Exception::RequestError, "Unable to submit request: #{e.errors.join(', ')}"
|
104
|
+
rescue
|
105
|
+
raise
|
106
|
+
end
|
107
|
+
|
108
|
+
request_id = response.headers[:location].split('/')[-1]
|
109
|
+
Vra::Request.new(client, request_id)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Chef Partner Engineering (<partnereng@chef.io>)
|
3
|
+
# Copyright:: Copyright (c) 2015 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'json'
|
20
|
+
require 'rest-client'
|
21
|
+
require 'passwordmasker'
|
22
|
+
|
23
|
+
module Vra
|
24
|
+
# rubocop:disable ClassLength
|
25
|
+
class Client
|
26
|
+
attr_accessor :page_size
|
27
|
+
|
28
|
+
def initialize(opts)
|
29
|
+
@base_url = opts[:base_url]
|
30
|
+
@username = opts[:username]
|
31
|
+
@password = PasswordMasker.new(opts[:password])
|
32
|
+
@tenant = opts[:tenant]
|
33
|
+
@verify_ssl = opts.fetch(:verify_ssl, true)
|
34
|
+
@bearer_token = PasswordMasker.new(nil)
|
35
|
+
@page_size = opts.fetch(:page_size, 20)
|
36
|
+
|
37
|
+
validate_client_options!
|
38
|
+
end
|
39
|
+
|
40
|
+
#########################
|
41
|
+
#
|
42
|
+
# methods to other classes
|
43
|
+
#
|
44
|
+
|
45
|
+
def catalog
|
46
|
+
Vra::Catalog.new(self)
|
47
|
+
end
|
48
|
+
|
49
|
+
def requests(*args)
|
50
|
+
Vra::Requests.new(self, *args)
|
51
|
+
end
|
52
|
+
|
53
|
+
def resources(*args)
|
54
|
+
Vra::Resources.new(self, *args)
|
55
|
+
end
|
56
|
+
|
57
|
+
#########################
|
58
|
+
#
|
59
|
+
# client methods
|
60
|
+
#
|
61
|
+
|
62
|
+
def bearer_token
|
63
|
+
@bearer_token.value
|
64
|
+
end
|
65
|
+
|
66
|
+
def bearer_token=(value)
|
67
|
+
@bearer_token.value = value
|
68
|
+
end
|
69
|
+
|
70
|
+
def bearer_token_request_body
|
71
|
+
{
|
72
|
+
'username' => @username,
|
73
|
+
'password' => @password.value,
|
74
|
+
'tenant' => @tenant
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def request_headers
|
79
|
+
headers = {}
|
80
|
+
headers['Accept'] = 'application/json'
|
81
|
+
headers['Content-Type'] = 'application/json'
|
82
|
+
headers['Authorization'] = "Bearer #{@bearer_token.value}" unless @bearer_token.value.nil?
|
83
|
+
headers
|
84
|
+
end
|
85
|
+
|
86
|
+
def authorize!
|
87
|
+
generate_bearer_token unless authorized?
|
88
|
+
|
89
|
+
raise Vra::Exception::Unauthorized, 'Unable to authorize against vRA' unless authorized?
|
90
|
+
end
|
91
|
+
|
92
|
+
def authorized?
|
93
|
+
return false if @bearer_token.value.nil?
|
94
|
+
|
95
|
+
response = http_head("/identity/api/tokens/#{@bearer_token.value}", :skip_auth)
|
96
|
+
if response.code == 204
|
97
|
+
true
|
98
|
+
else
|
99
|
+
false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def generate_bearer_token
|
104
|
+
@bearer_token.value = nil
|
105
|
+
validate_client_options!
|
106
|
+
|
107
|
+
response = http_post('/identity/api/tokens', bearer_token_request_body.to_json, :skip_auth)
|
108
|
+
if response.code != 200
|
109
|
+
raise Vra::Exception::Unauthorized, "Unable to get bearer token: #{response.body}"
|
110
|
+
end
|
111
|
+
|
112
|
+
@bearer_token.value = JSON.parse(response.body)['id']
|
113
|
+
end
|
114
|
+
|
115
|
+
def full_url(path)
|
116
|
+
"#{@base_url}#{path}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def http_head(path, skip_auth=nil)
|
120
|
+
authorize! unless skip_auth
|
121
|
+
|
122
|
+
response = RestClient::Request.execute(method: :head,
|
123
|
+
url: full_url(path),
|
124
|
+
headers: request_headers,
|
125
|
+
verify_ssl: @verify_ssl)
|
126
|
+
rescue => e
|
127
|
+
raise Vra::Exception::HTTPError, "head #{path} failed: #{e.class}: #{e.message}"
|
128
|
+
else
|
129
|
+
response
|
130
|
+
end
|
131
|
+
|
132
|
+
def http_get(path, skip_auth=nil)
|
133
|
+
authorize! unless skip_auth
|
134
|
+
|
135
|
+
response = RestClient::Request.execute(method: :get,
|
136
|
+
url: full_url(path),
|
137
|
+
headers: request_headers,
|
138
|
+
verify_ssl: @verify_ssl)
|
139
|
+
rescue => e
|
140
|
+
raise_http_exception(e, path)
|
141
|
+
else
|
142
|
+
response
|
143
|
+
end
|
144
|
+
|
145
|
+
def http_get!(path)
|
146
|
+
response = http_get(path)
|
147
|
+
response.body
|
148
|
+
end
|
149
|
+
|
150
|
+
def http_get_paginated_array!(path)
|
151
|
+
items = []
|
152
|
+
page = 1
|
153
|
+
base_path = path + "?limit=#{page_size}"
|
154
|
+
|
155
|
+
loop do
|
156
|
+
response = JSON.parse(http_get!("#{base_path}&page=#{page}"))
|
157
|
+
items += response['content']
|
158
|
+
|
159
|
+
break if page >= response['metadata']['totalPages']
|
160
|
+
page += 1
|
161
|
+
end
|
162
|
+
|
163
|
+
raise Vra::Exception::DuplicateItemsDetected,
|
164
|
+
'Duplicate items were returned by the vRA API. ' \
|
165
|
+
'Increase your page size to avoid this vRA API bug. ' \
|
166
|
+
'See https://github.com/nwops/vra-restapi#pagination ' \
|
167
|
+
'for more information.' if items.uniq!
|
168
|
+
|
169
|
+
items
|
170
|
+
end
|
171
|
+
|
172
|
+
def http_post(path, payload, skip_auth=nil)
|
173
|
+
authorize! unless skip_auth
|
174
|
+
|
175
|
+
response = RestClient::Request.execute(method: :post,
|
176
|
+
url: full_url(path),
|
177
|
+
headers: request_headers,
|
178
|
+
payload: payload,
|
179
|
+
verify_ssl: @verify_ssl)
|
180
|
+
rescue => e
|
181
|
+
raise_http_exception(e, path)
|
182
|
+
else
|
183
|
+
response
|
184
|
+
end
|
185
|
+
|
186
|
+
def http_post!(path, payload)
|
187
|
+
response = http_post(path, payload)
|
188
|
+
response.body
|
189
|
+
end
|
190
|
+
|
191
|
+
def raise_http_exception(caught_exception, path)
|
192
|
+
raise unless caught_exception.respond_to?(:http_code)
|
193
|
+
|
194
|
+
klass = if caught_exception.http_code == 404
|
195
|
+
Vra::Exception::HTTPNotFound
|
196
|
+
else
|
197
|
+
Vra::Exception::HTTPError
|
198
|
+
end
|
199
|
+
|
200
|
+
exception = klass.new(code: caught_exception.http_code,
|
201
|
+
body: caught_exception.response,
|
202
|
+
klass: caught_exception.class,
|
203
|
+
path: path)
|
204
|
+
|
205
|
+
message = exception.errors.empty? ? caught_exception.message : exception.errors.join(', ')
|
206
|
+
raise exception, message
|
207
|
+
end
|
208
|
+
|
209
|
+
def validate_client_options!
|
210
|
+
raise ArgumentError, 'Username and password are required' if @username.nil? || @password.value.nil?
|
211
|
+
raise ArgumentError, 'A tenant is required' if @tenant.nil?
|
212
|
+
raise ArgumentError, 'A base URL is required' if @base_url.nil?
|
213
|
+
raise ArgumentError, "Base URL #{@base_url} is not a valid URI." unless valid_uri?(@base_url)
|
214
|
+
end
|
215
|
+
|
216
|
+
def valid_uri?(uri)
|
217
|
+
uri = URI.parse(uri)
|
218
|
+
uri.is_a?(URI::HTTP)
|
219
|
+
rescue URI::InvalidURIError
|
220
|
+
false
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|