vcloud-rest 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ module VCloudClient
2
+ class Connection
3
+ ##
4
+ # Fetch details about a given catalog
5
+ def get_catalog(catalogId)
6
+ params = {
7
+ 'method' => :get,
8
+ 'command' => "/catalog/#{catalogId}"
9
+ }
10
+
11
+ response, headers = send_request(params)
12
+ description = response.css("Description").first
13
+ description = description.text unless description.nil?
14
+
15
+ items = {}
16
+ response.css("CatalogItem[type='application/vnd.vmware.vcloud.catalogItem+xml']").each do |item|
17
+ items[item['name']] = item['href'].gsub(/.*\/catalogItem\//, "")
18
+ end
19
+ { :id => catalogId, :description => description, :items => items }
20
+ end
21
+
22
+ ##
23
+ # Friendly helper method to fetch an catalog id by name
24
+ # - organization hash (from get_organization/get_organization_by_name)
25
+ # - catalog name
26
+ def get_catalog_id_by_name(organization, catalogName)
27
+ result = nil
28
+
29
+ organization[:catalogs].each do |catalog|
30
+ if catalog[0].downcase == catalogName.downcase
31
+ result = catalog[1]
32
+ end
33
+ end
34
+
35
+ result
36
+ end
37
+
38
+ ##
39
+ # Friendly helper method to fetch an catalog by name
40
+ # - organization hash (from get_organization/get_organization_by_name)
41
+ # - catalog name
42
+ def get_catalog_by_name(organization, catalogName)
43
+ result = nil
44
+
45
+ organization[:catalogs].each do |catalog|
46
+ if catalog[0].downcase == catalogName.downcase
47
+ result = get_catalog(catalog[1])
48
+ end
49
+ end
50
+
51
+ result
52
+ end
53
+
54
+ ##
55
+ # Fetch details about a given catalog item:
56
+ # - description
57
+ # - vApp templates
58
+ def get_catalog_item(catalogItemId)
59
+ params = {
60
+ 'method' => :get,
61
+ 'command' => "/catalogItem/#{catalogItemId}"
62
+ }
63
+
64
+ response, headers = send_request(params)
65
+ description = response.css("Description").first
66
+ description = description.text unless description.nil?
67
+
68
+ items = []
69
+ response.css("Entity[type='application/vnd.vmware.vcloud.vAppTemplate+xml']").each do |item|
70
+ itemId = item['href'].gsub(/.*\/vAppTemplate\/vappTemplate\-/, "")
71
+
72
+ # Fetch the catalogItemId information
73
+ params = {
74
+ 'method' => :get,
75
+ 'command' => "/vAppTemplate/vappTemplate-#{itemId}"
76
+ }
77
+ response, headers = send_request(params)
78
+
79
+ # VMs Hash for all the vApp VM entities
80
+ vms_hash = {}
81
+ response.css("/VAppTemplate/Children/Vm").each do |vmElem|
82
+ vmName = vmElem["name"]
83
+ vmId = vmElem["href"].gsub(/.*\/vAppTemplate\/vm\-/, "")
84
+
85
+ # Add the VM name/id to the VMs Hash
86
+ vms_hash[vmName] = { :id => vmId }
87
+ end
88
+
89
+ items << { :id => itemId,
90
+ :name => item['name'],
91
+ :vms_hash => vms_hash }
92
+ end
93
+ { :id => catalogItemId, :description => description, :items => items }
94
+ end
95
+
96
+ ##
97
+ # friendly helper method to fetch an catalogItem by name
98
+ # - catalogId (use get_catalog_name(org, name))
99
+ # - catalagItemName
100
+ def get_catalog_item_by_name(catalogId, catalogItemName)
101
+ result = nil
102
+ catalogElems = get_catalog(catalogId)
103
+
104
+ catalogElems[:items].each do |k, v|
105
+ if (k.downcase == catalogItemName.downcase)
106
+ result = get_catalog_item(v)
107
+ end
108
+ end
109
+
110
+ result
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,78 @@
1
+ module VCloudClient
2
+ class Connection
3
+ ##
4
+ # Fetch details about a given network
5
+ def get_network(networkId)
6
+ response = get_base_network(networkId)
7
+
8
+ name = response.css('OrgVdcNetwork').attribute('name').text
9
+
10
+ description = response.css("Description").first
11
+ description = description.text unless description.nil?
12
+
13
+ gateway = response.css('Gateway')
14
+ gateway = gateway.text unless gateway.nil?
15
+
16
+ netmask = response.css('Netmask')
17
+ netmask = netmask.text unless netmask.nil?
18
+
19
+ fence_mode = response.css('FenceMode')
20
+ fence_mode = fence_mode.text unless fence_mode.nil?
21
+
22
+ start_address = response.css('StartAddress')
23
+ start_address = start_address.text unless start_address.nil?
24
+
25
+ end_address = response.css('EndAddress')
26
+ end_address = end_address.text unless end_address.nil?
27
+
28
+
29
+ { :id => networkId, :name => name, :description => description,
30
+ :gateway => gateway, :netmask => netmask, :fence_mode => fence_mode,
31
+ :start_address => start_address, :end_address => end_address }
32
+ end
33
+
34
+ ##
35
+ # Friendly helper method to fetch an network id by name
36
+ # - organization hash (from get_organization/get_organization_by_name)
37
+ # - network name
38
+ def get_network_id_by_name(organization, networkName)
39
+ result = nil
40
+
41
+ organization[:networks].each do |network|
42
+ if network[0].downcase == networkName.downcase
43
+ result = network[1]
44
+ end
45
+ end
46
+
47
+ result
48
+ end
49
+
50
+ ##
51
+ # Friendly helper method to fetch an network by name
52
+ # - organization hash (from get_organization/get_organization_by_name)
53
+ # - network name
54
+ def get_network_by_name(organization, networkName)
55
+ result = nil
56
+
57
+ organization[:networks].each do |network|
58
+ if network[0].downcase == networkName.downcase
59
+ result = get_network(network[1])
60
+ end
61
+ end
62
+
63
+ result
64
+ end
65
+
66
+ private
67
+ # Get a network configuration
68
+ def get_base_network(networkId)
69
+ params = {
70
+ 'method' => :get,
71
+ 'command' => "/network/#{networkId}"
72
+ }
73
+
74
+ base_network, headers = send_request(params)
75
+ base_network
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,145 @@
1
+ module VCloudClient
2
+ class Connection
3
+ ##
4
+ # Fetch existing organizations and their IDs
5
+ def get_organizations
6
+ params = {
7
+ 'method' => :get,
8
+ 'command' => '/org'
9
+ }
10
+
11
+ response, headers = send_request(params)
12
+ orgs = response.css('OrgList Org')
13
+
14
+ results = {}
15
+ orgs.each do |org|
16
+ results[org['name']] = org['href'].gsub(/.*\/org\//, "")
17
+ end
18
+ results
19
+ end
20
+
21
+ ##
22
+ # friendly helper method to fetch an Organization Id by name
23
+ # - name (this isn't case sensitive)
24
+ def get_organization_id_by_name(name)
25
+ result = nil
26
+
27
+ # Fetch all organizations
28
+ organizations = get_organizations()
29
+
30
+ organizations.each do |organization|
31
+ if organization[0].downcase == name.downcase
32
+ result = organization[1]
33
+ end
34
+ end
35
+ result
36
+ end
37
+
38
+
39
+ ##
40
+ # friendly helper method to fetch an Organization by name
41
+ # - name (this isn't case sensitive)
42
+ def get_organization_by_name(name)
43
+ result = nil
44
+
45
+ # Fetch all organizations
46
+ organizations = get_organizations()
47
+
48
+ organizations.each do |organization|
49
+ if organization[0].downcase == name.downcase
50
+ result = get_organization(organization[1])
51
+ end
52
+ end
53
+ result
54
+ end
55
+
56
+ ##
57
+ # Fetch details about an organization:
58
+ # - catalogs
59
+ # - vdcs
60
+ # - networks
61
+ # - task lists
62
+ def get_organization(orgId)
63
+ params = {
64
+ 'method' => :get,
65
+ 'command' => "/org/#{orgId}"
66
+ }
67
+
68
+ response, headers = send_request(params)
69
+ catalogs = {}
70
+ response.css("Link[type='application/vnd.vmware.vcloud.catalog+xml']").each do |item|
71
+ catalogs[item['name']] = item['href'].gsub(/.*\/catalog\//, "")
72
+ end
73
+
74
+ vdcs = {}
75
+ response.css("Link[type='application/vnd.vmware.vcloud.vdc+xml']").each do |item|
76
+ vdcs[item['name']] = item['href'].gsub(/.*\/vdc\//, "")
77
+ end
78
+
79
+ networks = {}
80
+ response.css("Link[type='application/vnd.vmware.vcloud.orgNetwork+xml']").each do |item|
81
+ networks[item['name']] = item['href'].gsub(/.*\/network\//, "")
82
+ end
83
+
84
+ tasklists = {}
85
+ response.css("Link[type='application/vnd.vmware.vcloud.tasksList+xml']").each do |item|
86
+ tasklists[item['name']] = item['href'].gsub(/.*\/tasksList\//, "")
87
+ end
88
+
89
+ { :catalogs => catalogs, :vdcs => vdcs, :networks => networks, :tasklists => tasklists }
90
+ end
91
+
92
+ ##
93
+ # Fetch tasks from a given task list
94
+ #
95
+ # Note: id can be retrieved using get_organization
96
+ def get_tasks_list(id)
97
+ params = {
98
+ 'method' => :get,
99
+ 'command' => "/tasksList/#{id}"
100
+ }
101
+
102
+ response, headers = send_request(params)
103
+
104
+ tasks = []
105
+
106
+ response.css('Task').each do |task|
107
+ id = task['href'].gsub(/.*\/task\//, "")
108
+ operation = task['operationName']
109
+ status = task['status']
110
+ error = nil
111
+ error = task.css('Error').first['message'] if task['status'] == 'error'
112
+ start_time = task['startTime']
113
+ end_time = task['endTime']
114
+ user_canceled = task['cancelRequested'] == 'true'
115
+
116
+ tasks << {
117
+ :id => id,
118
+ :operation => operation,
119
+ :status => status,
120
+ :error => error,
121
+ :start_time => start_time,
122
+ :end_time => end_time,
123
+ :user_canceled => user_canceled
124
+ }
125
+ end
126
+ tasks
127
+ end
128
+
129
+ ##
130
+ # Cancel a given task
131
+ #
132
+ # The task will be marked for cancellation
133
+ def cancel_task(id)
134
+ params = {
135
+ 'method' => :post,
136
+ 'command' => "/task/#{id}/action/cancel"
137
+ }
138
+
139
+ # Nothing useful is returned here
140
+ # If return code is 20x return true
141
+ send_request(params)
142
+ true
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,251 @@
1
+ module VCloudClient
2
+ class Connection
3
+ ##
4
+ # Upload an OVF package
5
+ # - vdcId
6
+ # - vappName
7
+ # - vappDescription
8
+ # - ovfFile
9
+ # - catalogId
10
+ # - uploadOptions {}
11
+ def upload_ovf(vdcId, vappName, vappDescription, ovfFile, catalogId, uploadOptions={})
12
+ raise ::IOError, "OVF #{ovfFile} is missing." unless File.exists?(ovfFile)
13
+
14
+ # if send_manifest is not set, setting it true
15
+ if uploadOptions[:send_manifest].nil? || uploadOptions[:send_manifest]
16
+ uploadManifest = "true"
17
+ else
18
+ uploadManifest = "false"
19
+ end
20
+
21
+ builder = Nokogiri::XML::Builder.new do |xml|
22
+ xml.UploadVAppTemplateParams(
23
+ "xmlns" => "http://www.vmware.com/vcloud/v1.5",
24
+ "xmlns:ovf" => "http://schemas.dmtf.org/ovf/envelope/1",
25
+ "manifestRequired" => uploadManifest,
26
+ "name" => vappName) {
27
+ xml.Description vappDescription
28
+ }
29
+ end
30
+
31
+ params = {
32
+ 'method' => :post,
33
+ 'command' => "/vdc/#{vdcId}/action/uploadVAppTemplate"
34
+ }
35
+
36
+ response, headers = send_request(
37
+ params,
38
+ builder.to_xml,
39
+ "application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml"
40
+ )
41
+
42
+ # Get vAppTemplate Link from location
43
+ vAppTemplate = headers[:location].gsub(/.*\/vAppTemplate\/vappTemplate\-/, "")
44
+ descriptorUpload = response.css("Files Link [rel='upload:default']").first[:href].gsub("#{@host_url}/transfer/", "")
45
+ transferGUID = descriptorUpload.gsub("/descriptor.ovf", "")
46
+
47
+ ovfFileBasename = File.basename(ovfFile, ".ovf")
48
+ ovfDir = File.dirname(ovfFile)
49
+
50
+ # Send OVF Descriptor
51
+ uploadURL = "/transfer/#{descriptorUpload}"
52
+ uploadFile = "#{ovfDir}/#{ovfFileBasename}.ovf"
53
+ upload_file(uploadURL, uploadFile, vAppTemplate, uploadOptions)
54
+
55
+ @logger.debug "OVF Descriptor uploaded."
56
+
57
+ # Begin the catch for upload interruption
58
+ begin
59
+ params = {
60
+ 'method' => :get,
61
+ 'command' => "/vAppTemplate/vappTemplate-#{vAppTemplate}"
62
+ }
63
+
64
+ # Loop to wait for the upload links to show up in the vAppTemplate we just created
65
+ while true
66
+ response, headers = send_request(params)
67
+
68
+ errored_task = response.css("Tasks Task [status='error']").first
69
+ if errored_task
70
+ error_msg = errored_task.css('Error').first['message']
71
+ raise OVFError, "OVF Upload failed: #{error_msg}"
72
+ end
73
+
74
+ break unless response.css("Files Link [rel='upload:default']").count == 1
75
+ sleep 1
76
+ end
77
+
78
+ if uploadManifest == "true"
79
+ uploadURL = "/transfer/#{transferGUID}/descriptor.mf"
80
+ uploadFile = "#{ovfDir}/#{ovfFileBasename}.mf"
81
+ upload_file(uploadURL, uploadFile, vAppTemplate, uploadOptions)
82
+ @logger.debug "OVF Manifest uploaded."
83
+ end
84
+
85
+ # Start uploading OVF VMDK files
86
+ params = {
87
+ 'method' => :get,
88
+ 'command' => "/vAppTemplate/vappTemplate-#{vAppTemplate}"
89
+ }
90
+ response, headers = send_request(params)
91
+ response.css("Files File [bytesTransferred='0'] Link [rel='upload:default']").each do |file|
92
+ fileName = file[:href].gsub("#{@host_url}/transfer/#{transferGUID}/","")
93
+ uploadFile = "#{ovfDir}/#{fileName}"
94
+ uploadURL = "/transfer/#{transferGUID}/#{fileName}"
95
+ upload_file(uploadURL, uploadFile, vAppTemplate, uploadOptions)
96
+ end
97
+
98
+ # Add item to the catalog catalogId
99
+ builder = Nokogiri::XML::Builder.new do |xml|
100
+ xml.CatalogItem(
101
+ "xmlns" => "http://www.vmware.com/vcloud/v1.5",
102
+ "type" => "application/vnd.vmware.vcloud.catalogItem+xml",
103
+ "name" => vappName) {
104
+ xml.Description vappDescription
105
+ xml.Entity(
106
+ "href" => "#{@api_url}/vAppTemplate/vappTemplate-#{vAppTemplate}"
107
+ )
108
+ }
109
+ end
110
+
111
+ params = {
112
+ 'method' => :post,
113
+ 'command' => "/catalog/#{catalogId}/catalogItems"
114
+ }
115
+
116
+ @logger.debug "Add item to catalog."
117
+ response, headers = send_request(params, builder.to_xml,
118
+ "application/vnd.vmware.vcloud.catalogItem+xml")
119
+
120
+ entity = response.css("Entity").first
121
+
122
+ # TODO: the best thing would detect the real importing status.
123
+ result = {}
124
+ if entity
125
+ result[:id] = entity['href'].gsub(/.*\/vAppTemplate\/vappTemplate\-/, "")
126
+ result[:name] = entity['name']
127
+ end
128
+ result
129
+ rescue Exception => e
130
+ @logger.error "Exception detected: #{e.message}."
131
+
132
+ # Get vAppTemplate Task
133
+ params = {
134
+ 'method' => :get,
135
+ 'command' => "/vAppTemplate/vappTemplate-#{vAppTemplate}"
136
+ }
137
+ response, headers = send_request(params)
138
+
139
+ # Cancel Task
140
+ # Note that it might not exist (i.e., error for existing vdc entity)
141
+ tasks = response.css("Tasks")
142
+ unless tasks.empty?
143
+ tasks.css("Task").each do |task|
144
+ if task['status'] == 'error'
145
+ @logger.error task.css('Error').first['message']
146
+ else
147
+ id = task['href'].gsub(/.*\/task\//, "")
148
+ @logger.error "Aborting task #{id}..."
149
+ cancel_task(id)
150
+ end
151
+ end
152
+ end
153
+
154
+ raise e
155
+ end
156
+ end
157
+
158
+ private
159
+ ##
160
+ # Upload a large file in configurable chunks, output an optional progressbar
161
+ def upload_file(uploadURL, uploadFile, vAppTemplate, config={})
162
+ raise ::IOError, "#{uploadFile} not found." unless File.exists?(uploadFile)
163
+
164
+ # Set chunksize to 10M if not specified otherwise
165
+ chunkSize = (config[:chunksize] || 10485760)
166
+
167
+ # Set progress bar to default format if not specified otherwise
168
+ progressBarFormat = (config[:progressbar_format] || "%e <%B> %p%% %t")
169
+
170
+ # Set progress bar length to 120 if not specified otherwise
171
+ progressBarLength = (config[:progressbar_length] || 120)
172
+
173
+ # Open our file for upload
174
+ uploadFileHandle = File.new(uploadFile, "rb" )
175
+ fileName = File.basename(uploadFileHandle)
176
+
177
+ progressBarTitle = "Uploading: " + uploadFile.to_s
178
+
179
+ # Create a progressbar object if progress bar is enabled
180
+ if config[:progressbar_enable] == true && uploadFileHandle.size.to_i > chunkSize
181
+ progressbar = ProgressBar.create(
182
+ :title => progressBarTitle,
183
+ :starting_at => 0,
184
+ :total => uploadFileHandle.size.to_i,
185
+ :length => progressBarLength,
186
+ :format => progressBarFormat
187
+ )
188
+ else
189
+ @logger.info progressBarTitle
190
+ end
191
+ # Create a new HTTP client
192
+ clnt = HTTPClient.new
193
+
194
+ # Disable SSL cert verification
195
+ clnt.ssl_config.verify_mode=(OpenSSL::SSL::VERIFY_NONE)
196
+
197
+ # Suppress SSL depth message
198
+ clnt.ssl_config.verify_callback=proc{ |ok, ctx|; true };
199
+
200
+ # Perform ranged upload until the file reaches its end
201
+ until uploadFileHandle.eof?
202
+
203
+ # Create ranges for this chunk upload
204
+ rangeStart = uploadFileHandle.pos
205
+ rangeStop = uploadFileHandle.pos.to_i + chunkSize
206
+
207
+ # Read current chunk
208
+ fileContent = uploadFileHandle.read(chunkSize)
209
+
210
+ # If statement to handle last chunk transfer if is > than filesize
211
+ if rangeStop.to_i > uploadFileHandle.size.to_i
212
+ contentRange = "bytes #{rangeStart.to_s}-#{uploadFileHandle.size.to_s}/#{uploadFileHandle.size.to_s}"
213
+ rangeLen = uploadFileHandle.size.to_i - rangeStart.to_i
214
+ else
215
+ contentRange = "bytes #{rangeStart.to_s}-#{rangeStop.to_s}/#{uploadFileHandle.size.to_s}"
216
+ rangeLen = rangeStop.to_i - rangeStart.to_i
217
+ end
218
+
219
+ # Build headers
220
+ extheader = {
221
+ 'x-vcloud-authorization' => @auth_key,
222
+ 'Content-Range' => contentRange,
223
+ 'Content-Length' => rangeLen.to_s
224
+ }
225
+
226
+ begin
227
+ uploadRequest = "#{@host_url}#{uploadURL}"
228
+ connection = clnt.request('PUT', uploadRequest, nil, fileContent, extheader)
229
+
230
+ if config[:progressbar_enable] == true && uploadFileHandle.size.to_i > chunkSize
231
+ params = {
232
+ 'method' => :get,
233
+ 'command' => "/vAppTemplate/vappTemplate-#{vAppTemplate}"
234
+ }
235
+ response, headers = send_request(params)
236
+
237
+ response.css("Files File [name='#{fileName}']").each do |file|
238
+ progressbar.progress=file[:bytesTransferred].to_i
239
+ end
240
+ end
241
+ rescue
242
+ retryTime = (config[:retry_time] || 5)
243
+ @logger.warn "Range #{contentRange} failed to upload, retrying the chunk in #{retryTime.to_s} seconds, to stop the action press CTRL+C."
244
+ sleep retryTime.to_i
245
+ retry
246
+ end
247
+ end
248
+ uploadFileHandle.close
249
+ end
250
+ end
251
+ end