# # Copyright 2012 Stefano Tortarolo # Copyright 2013 Fabio Rapposelli and Timo Sugliani # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'log4r' require 'vagrant/util/busy' require 'vagrant/util/platform' require 'vagrant/util/retryable' require 'vagrant/util/subprocess' require 'awesome_print' module VagrantPlugins module VCloud module Driver class UnauthorizedAccess < StandardError; end class WrongAPIVersion < StandardError; end class WrongItemIDError < StandardError; end class InvalidStateError < StandardError; end class InternalServerError < StandardError; end class UnhandledError < StandardError; end # Main class to access vCloud rest APIs class Base include Vagrant::Util::Retryable def initialize @logger = Log4r::Logger.new('vagrant::provider::vcloud::base') end ## # Authenticate against the specified server def login end ## # Destroy the current session def logout end ## # Fetch existing organizations and their IDs def get_organizations end ## # friendly helper method to fetch an Organization Id by name # - name (this isn't case sensitive) def get_organization_id_by_name(name) end ## # friendly helper method to fetch an Organization by name # - name (this isn't case sensitive) def get_organization_by_name(name) end ## # Fetch details about an organization: # - catalogs # - vdcs # - networks def get_organization(org_id) end ## # Fetch details about a given catalog def get_catalog(catalog_id) end ## # Friendly helper method to fetch an catalog id by name # - organization hash (from get_organization/get_organization_by_name) # - catalog name def get_catalog_id_by_name(organization, catalog_name) end ## # Friendly helper method to fetch an catalog by name # - organization hash (from get_organization/get_organization_by_name) # - catalog name def get_catalog_by_name(organization, catalog_name) end ## # Fetch details about a given vdc: # - description # - vapps # - networks def get_vdc(vdc_id) end ## # Friendly helper method to fetch a Organization VDC Id by name # - Organization object # - Organization VDC Name def get_vdc_id_by_name(organization, vdc_name) end ## # Friendly helper method to fetch a Organization VDC by name # - Organization object # - Organization VDC Name def get_vdc_by_name(organization, vdc_name) end ## # Fetch details about a given catalog item: # - description # - vApp templates def get_catalog_item(catalog_item_id) end ## # friendly helper method to fetch an catalogItem by name # - catalogId (use get_catalog_name(org, name)) # - catalagItemName def get_catalog_item_by_name(catalog_id, catalog_item_name) end ## # Fetch details about a given vapp: # - name # - description # - status # - IP # - Children VMs: # -- IP addresses # -- status # -- ID def get_vapp(vapp_id) end ## # Delete a given vapp # NOTE: It doesn't verify that the vapp is shutdown def delete_vapp(vapp_id) end ## # Suspend a given vapp def suspend_vapp(vapp_id) end ## # reboot a given vapp # This will basically initial a guest OS reboot, and will only work if # VMware-tools are installed on the underlying VMs. # vShield Edge devices are not affected def reboot_vapp(vapp_id) end ## # reset a given vapp # This will basically reset the VMs within the vApp # vShield Edge devices are not affected. def reset_vapp(vapp_id) end ## # Boot a given vapp def poweron_vapp(vapp_id) end ## # Create a vapp starting from a template # # Params: # - vdc: the associated VDC # - vapp_name: name of the target vapp # - vapp_description: description of the target vapp # - vapp_templateid: ID of the vapp template def create_vapp_from_template(vdc, vapp_name, vapp_description, vapp_templateid, poweron = false) end ## # Compose a vapp using existing virtual machines # # Params: # - vdc: the associated VDC # - vapp_name: name of the target vapp # - vapp_description: description of the target vapp # - vm_list: hash with IDs of the VMs used in the composing process # - network_config: hash of the network configuration for the vapp def compose_vapp_from_vm(vdc, vapp_name, vapp_description, vm_list = {}, network_config = {}) end # Fetch details about a given vapp template: # - name # - description # - Children VMs: # -- ID def get_vapp_template(vapp_id) end ## # Set vApp port forwarding rules # # - vappid: id of the vapp to be modified # - network_name: name of the vapp network to be modified # - config: hash with network configuration specifications, must contain # an array inside :nat_rules with the nat rules to be applied. def set_vapp_port_forwarding_rules(vapp_id, network_name, config = {}) end ## # Get vApp port forwarding rules # # - vappid: id of the vApp def get_vapp_port_forwarding_rules(vapp_id) end ## # get vApp edge public IP from the vApp ID # Only works when: # - vApp needs to be poweredOn # - FenceMode is set to "natRouted" # - NatType" is set to "portForwarding # This will be required to know how to connect to VMs behind the Edge. def get_vapp_edge_public_ip(vapp_id) end ## # Upload an OVF package # - vdcId # - vappName # - vappDescription # - ovfFile # - catalogId # - uploadOptions {} def upload_ovf(vdc_id, vapp_name, vapp_description, ovf_file, catalog_id, upload_options = {}) end ## # Fetch information for a given task def get_task(task_id) end ## # Poll a given task until completion def wait_task_completion(task_id) end ## # Set vApp Network Config def set_vapp_network_config(vapp_id, network_name, config = {}) end ## # Set VM Network Config def set_vm_network_config(vm_id, network_name, config = {}) end ## # Set VM Guest Customization Config def set_vm_guest_customization(vm_id, computer_name, config = {}) end ## # Fetch details about a given VM def get_vm(vm_Id) end private ## # Sends a synchronous request to the vCloud API and returns the # response as parsed XML + headers using HTTPClient. def send_request(params, payload = nil, content_type = nil) # Create a new HTTP client clnt = HTTPClient.new # Disable SSL cert verification clnt.ssl_config.verify_mode = (OpenSSL::SSL::VERIFY_NONE) # Suppress SSL depth message clnt.ssl_config.verify_callback = proc { |ok, ctx|; true } extheader = {} extheader['accept'] = "application/*+xml;version=#{@api_version}" if !content_type.nil? extheader['Content-Type'] = content_type end if @auth_key extheader['x-vcloud-authorization'] = @auth_key else clnt.set_auth(nil, "#{@username}@#{@org_name}", @password) end url = "#{@api_url}#{params['command']}" # Massive debug when LOG=DEBUG # Using awesome_print to get nice XML output for better readability if @logger.level == 1 ap "[#{Time.now.ctime}] -> SEND #{params['method'].upcase} #{url}" if payload payload_xml = Nokogiri.XML(payload) ap 'SEND HEADERS' ap extheader ap 'SEND BODY' ap payload_xml end end begin response = clnt.request( params['method'], url, nil, payload, extheader ) if !response.ok? raise "Warning: unattended code #{response.status}" + " #{response.reason}" end nicexml = Nokogiri.XML(response.body) # Massive debug when LOG=DEBUG # Using awesome_print to get nice XML output for readability if @logger.level == 1 ap "[#{Time.now.ctime}] <- RECV #{response.status}" # Just avoid the task spam. if !url.index('/task/') ap 'RECV HEADERS' ap response.headers ap 'RECV BODY' ap nicexml end end [Nokogiri.parse(response.body), response.headers] rescue SocketError raise 'Impossible to connect, verify endpoint' rescue Errno::EADDRNOTAVAIL raise 'Impossible to connect, verify endpoint' end end ## # Upload a large file in configurable chunks, output an optional # progressbar def upload_file(upload_url, upload_file, vapp_template, config = {}) # Set chunksize to 1M if not specified otherwise chunk_size = (config[:chunksize] || 1_048_576) @logger.debug("Set chunksize to #{chunk_size} bytes") # Set progressbar to default format if not specified otherwise progressbar_format = ( config[:progressbar_format] || '%t Progress: %p%% %e' ) # Open our file for upload upload_file_handle = File.new(upload_file, 'rb') file_name = File.basename(upload_file_handle) # FIXME: I removed the filename below because I recall a weird issue # of upload failing because if a too long filename # (tsugliani) progressbar_title = 'Uploading Box...' # Create a progressbar object if progress bar is enabled if config[:progressbar_enable] == true && upload_file_handle.size.to_i > chunk_size progressbar = ProgressBar.create( :title => progressbar_title, :starting_at => 0, :total => upload_file_handle.size.to_i, :format => progressbar_format ) else puts progressbar_title end # Create a new HTTP client clnt = HTTPClient.new # Disable SSL cert verification clnt.ssl_config.verify_mode = (OpenSSL::SSL::VERIFY_NONE) # Suppress SSL depth message clnt.ssl_config.verify_callback = proc { |ok, ctx|; true } # Perform ranged upload until the file reaches its end until upload_file_handle.eof? # Create ranges for this chunk upload range_start = upload_file_handle.pos range_stop = upload_file_handle.pos.to_i + chunk_size # Read current chunk file_content = upload_file_handle.read(chunk_size) # If statement to handle last chunk transfer if is > than filesize if range_stop.to_i > upload_file_handle.size.to_i content_range = "bytes #{range_start.to_s}-" + "#{upload_file_handle.size.to_s}/" + "#{upload_file_handle.size.to_s}" range_len = upload_file_handle.size.to_i - range_start.to_i else content_range = "bytes #{range_start.to_s}-" + "#{range_stop.to_s}/" + "#{upload_file_handle.size.to_s}" range_len = range_stop.to_i - range_start.to_i end # Build headers extheader = { 'x-vcloud-authorization' => @auth_key, 'Content-Range' => content_range, 'Content-Length' => range_len.to_s } begin upload_request = "#{@host_url}#{upload_url}" # FIXME: Add debug on the return status of "connection" # to enhance troubleshooting for this upload process. # (tsugliani) _connection = clnt.request( 'PUT', upload_request, nil, file_content, extheader ) if config[:progressbar_enable] == true && upload_file_handle.size.to_i > chunk_size params = { 'method' => :get, 'command' => "/vAppTemplate/vappTemplate-#{vapp_template}" } response, _headers = send_request(params) response.css( "Files File [name='#{file_name}']" ).each do |file| progressbar.progress = file[:bytesTransferred].to_i end end rescue # FIXME: HUGE FIXME!!!! # DO SOMETHING WITH THIS, IT'S JUST STUPID AS IT IS NOW!!! retry_time = (config[:retry_time] || 5) puts "Range #{content_range} failed to upload, " + "retrying the chunk in #{retry_time.to_s} seconds, " + 'to stop this task press CTRL+C.' sleep retry_time.to_i retry end end upload_file_handle.close end ## # Convert vApp status codes into human readable description def convert_vapp_status(status_code) case status_code.to_i when 0 'suspended' when 3 'paused' when 4 'running' when 8 'stopped' when 10 'mixed' else "Unknown #{status_code}" end end end # class end end end