# # 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 VCloudAir 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 Air rest APIs class Base include Vagrant::Util::Retryable def initialize @logger = Log4r::Logger.new('vagrant::provider::vcloudair::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 def set_vm_hardware(vm_id, cfg) 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 Air API and returns the # response as parsed XML + headers using HTTPClient. def send_vcloudair_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) # Set SSL proto to TLSv1 clnt.ssl_config.ssl_version = :TLSv1 # Suppress SSL depth message clnt.ssl_config.verify_callback = proc { |ok, ctx|; true } extheader = {} extheader['accept'] = 'application/xml;version=5.6' unless content_type.nil? extheader['Content-Type'] = content_type end if @vcloudair_auth_key @logger.debug("vCloud Air authorization key: #{@vcloudair_auth_key}") extheader['x-vchs-authorization'] = @vcloudair_auth_key else @logger.debug('vCloud Air authorization not set') @logger.debug("Sending username: #{@username} and password: #{@password}") extheader['Authorization'] = "Basic " + Base64.strict_encode64("#{@username}:#{@password}") # clnt.set_auth(nil, @username, @password) end url = "https://vchs.vmware.com/api#{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}" ap 'SEND HEADERS' ap extheader if payload payload_xml = Nokogiri.XML(payload) ap 'SEND BODY' ap payload_xml end end begin response = clnt.request( params['method'], url, nil, payload, extheader ) unless response.ok? case response.code when 400 error_message = Nokogiri.parse(response.body) error = error_message.css('Error') fail Errors::InvalidRequestError, :message => error.first['message'].to_s when 401 fail Errors::UnauthorizedAccess, :message => response.status else fail Errors::UnattendedCodeError, :message => response.status end 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. unless url.index('/task/') ap 'RECV HEADERS' ap response.headers ap 'RECV BODY' ap nicexml end end [Nokogiri.parse(response.body), response.headers] rescue SocketError, Errno::EADDRNOTAVAIL raise Errors::EndpointUnavailable end end def get_api_version(host_url) # Create a new HTTP client clnt = HTTPClient.new # Disable SSL cert verification # clnt.ssl_config.verify_mode = (OpenSSL::SSL::VERIFY_NONE) # Set SSL proto to TLSv1 clnt.ssl_config.ssl_version = :TLSv1 # Suppress SSL depth message clnt.ssl_config.verify_callback = proc { |ok, ctx|; true } uri = URI(host_url) url = "#{uri.scheme}://#{uri.host}:#{uri.port}/api/versions" begin response = clnt.request('GET', url, nil, nil, nil) unless response.ok? fail Errors::UnattendedCodeError, :message => response.status + ' ' + response.reason end version_info = Nokogiri.parse(response.body) api_version = version_info.css('VersionInfo Version') api_version_supported = 0.0 # Go through each available Version and return the latest supported # version api_version.each do |api_available_version| if api_version_supported.to_f < api_available_version.text.to_f api_version_supported = api_available_version.text end end api_version_supported rescue SocketError, Errno::EADDRNOTAVAIL raise Errors::EndpointUnavailable end end ## # Sends a synchronous request to the vCloud Air 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 # Set SSL proto to TLSv1 clnt.ssl_config.ssl_version = :TLSv1 # Suppress SSL depth message clnt.ssl_config.verify_callback = proc { |ok, ctx|; true } extheader = {} extheader['accept'] = "application/*+xml;version=#{@api_version}" extheader['Content-Type'] = content_type unless content_type.nil? 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 ) unless response.ok? if response.code == 400 error_message = Nokogiri.parse(response.body) error = error_message.css('Error') fail Errors::InvalidRequestError, :message => error.first['message'].to_s else fail Errors::UnattendedCodeError, :message => response.status end 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. unless url.index('/task/') ap 'RECV HEADERS' ap response.headers ap 'RECV BODY' ap nicexml end end [Nokogiri.parse(response.body), response.headers] rescue SocketError, Errno::EADDRNOTAVAIL, Errno::ETIMEDOUT raise Errors::EndpointUnavailable 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 5M if not specified otherwise chunk_size = (config[:chunksize] || 5_242_880) @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) # => Added the filename back, needs more testing (frapposelli) progressbar_title = "Uploading #{file_name}" # 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 # Set SSL proto to TLSv1 clnt.ssl_config.ssl_version = :TLSv1 # 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 } upload_request = "#{@host_url}#{upload_url}" # 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 PUT #{upload_request}" ap 'SEND HEADERS' ap extheader ap 'SEND BODY' ap '' end begin response = clnt.request( 'PUT', upload_request, nil, file_content, extheader ) unless response.ok? fail Errors::UnattendedCodeError, :message => response.status end 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