lib/vagrant-vcloudair/driver/base.rb
#
# 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 '<data omitted>'
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