gosddc/vagrant-vcloudair

View on GitHub
lib/vagrant-vcloudair/driver/base.rb

Summary

Maintainability
F
3 days
Test Coverage
#
#  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