cloudfoundry-community/bosh-cloudstack-cpi

View on GitHub
bosh_agent/lib/bosh_agent/infrastructure/cloudstack/registry.rb

Summary

Maintainability
C
1 day
Test Coverage
# Copyright (c) 2009-2013 VMware, Inc.
# Copyright (c) 2013 Nippon Telegraph and Telephone Corporation

module Bosh::Agent
  class Infrastructure::Cloudstack::Registry
    class << self

      attr_accessor :user_data

      HTTP_API_TIMEOUT     = 300
      HTTP_CONNECT_TIMEOUT = 30

      ##
      # Returns the logger.
      #
      # @return [Logger] Bosh Agent logger
      def logger
        Bosh::Agent::Config.logger
      end

      ##
      # Gets the OpenSSH public key. First we try to get it from the CloudStack meta data endpoint, if we fail,
      # then we fallback to the injected user data file.
      #
      # @return [String] OpenSSH key
      def get_openssh_key
        get_uri(meta_data_uri + "/public-keys")
      rescue LoadSettingsError => e
        logger.info("Failed to get OpenSSH public key from CloudStack meta data endpoint: #{e.message}")
        user_data = parse_user_data(get_user_data_from_file)
        unless user_data.has_key?("openssh") && user_data["openssh"].has_key?("public_key")
          raise LoadSettingsError, "Cannot get OpenSSH public key from injected user data file: #{user_data.inspect}"
        end
        user_data["openssh"]["public_key"]
      end

      ##
      # Gets the settings for this agent from the Bosh registry.
      #
      # @return [Hash] Agent Settings
      def get_settings
        @registry_endpoint ||= get_registry_endpoint
        url = "#{@registry_endpoint}/instances/#{get_server_name}/settings"
        raw_response = get_uri(url)

        registry_data = Yajl::Parser.parse(raw_response)
        unless registry_data.is_a?(Hash) && registry_data.has_key?("settings")
          raise LoadSettingsError, "Invalid response received from Bosh registry, " \
                                   "got #{registry_data.class}: #{registry_data}"
        end

        settings = Yajl::Parser.parse(registry_data["settings"])
        unless settings.is_a?(Hash)
          raise(LoadSettingsError, "Invalid settings received from Bosh registry, " \
                                   "got #{settings.class}: #{settings}")
        end

        settings
      rescue Yajl::ParseError => e
        raise LoadSettingsError, "Cannot parse settings from Bosh registry, got #{raw_response} - #{e.message}"
      end

      def meta_data_uri
        @@meta_data_uri ||= "http://" +
          %x[grep dhcp-server-identifier /var/lib/dhclient/* /var/lib/dhcp3/* /var/lib/dhcp/* 2>/dev/null | tail -1 | awk '{print $NF}' | tr -d '\;'].strip +
          "/latest"
      end

      ##
      # Gets the server name from CloudStack user data.
      #
      # @return [String] CloudStack server name
      def get_server_name
        user_data = get_user_data
        unless user_data.has_key?("server") && user_data["server"].has_key?("name")
          raise LoadSettingsError, "Cannot get CloudStack server name from user data #{user_data.inspect}"
        end
        user_data["server"]["name"]
      end

      ##
      # Gets the Bosh registry endpoint from CloudStack user data.
      #
      # @return [String] Bosh registry endpoint
      def get_registry_endpoint
        user_data = get_user_data
        unless user_data.has_key?("registry") && user_data["registry"].has_key?("endpoint")
          raise LoadSettingsError, "Cannot get Bosh registry endpoint from user data #{user_data.inspect}"
        end
        lookup_registry_endpoint(user_data)
      end

      ##
      # If the Bosh registry endpoint is specified with a Bosh DNS name, i.e. 0.registry.default.cloudstack.bosh,
      # then the agent needs to lookup the name and insert the IP address, as the agent doesn't update
      # resolv.conf until after the bootstrap is run.
      #
      # @param [Hash] user_data CloudStack user data (generated by the CPI)
      # @return [String] Bosh registry endpoint
      def lookup_registry_endpoint(user_data)
        registry_endpoint = user_data["registry"]["endpoint"]

        # If user data doesn't contain dns info, there is noting we can do, so just return the endpoint
        return registry_endpoint if user_data["dns"].nil? || user_data["dns"]["nameserver"].nil?

        # If the endpoint is an IP address, just return the endpoint
        registry_hostname = extract_registry_hostname(registry_endpoint)
        return registry_endpoint unless (IPAddr.new(registry_hostname) rescue(nil)).nil?

        nameservers = user_data["dns"]["nameserver"]
        registry_ip = lookup_registry_ip_address(registry_hostname, nameservers)
        inject_registry_ip_address(registry_ip, registry_endpoint)
      rescue Resolv::ResolvError => e
        raise LoadSettingsError, "Cannot lookup #{registry_hostname} using #{nameservers.join(", ")}: #{e.inspect}"
      end

      ##
      # Extracts the hostname from the Bosh registry endpoint.
      #
      # @param [String] endpoint Bosh registry endpoint
      # @return [String] Bosh registry hostname
      def extract_registry_hostname(endpoint)
        match = endpoint.match(%r{https*://([^:]+):})
        unless match && match.size == 2
          raise LoadSettingsError, "Cannot extract Bosh registry hostname from #{endpoint}"
        end
        match[1]
      end

      ##
      # Lookups for the Bosh registry IP address.
      #
      # @param [String] hostname Bosh registry hostname
      # @param [Array] nameservers Array containing nameserver address
      # @return [Resolv::IPv4] Bosh registry IP address
      def lookup_registry_ip_address(hostname, nameservers)
        resolver = Resolv::DNS.new(:nameserver => nameservers)
        resolver.getaddress(hostname)
      end

      ##
      # Injects an IP address in the Bosh registry endpoint.
      #
      # @param [Resolv::IPv4] ip Bosh registry IP address
      # @param [String] endpoint Bosh registry endpoint
      # @return [String] Bosh registry endpoint
      def inject_registry_ip_address(ip, endpoint)
        endpoint.sub(%r{//[^:]+:}, "//#{ip}:")
      end

      ##
      # Gets the CloudStack user data. First we try to get it from the CloudStack user data endpoint, if we fail,
      # then we fallback to the injected user data file.
      #
      # @return [Hash] CloudStack user data
      def get_user_data
        return @user_data if @user_data
        begin
          raw_user_data = get_uri(meta_data_uri + "/user-data")
        rescue LoadSettingsError => e
          logger.info("Failed to get user data from CloudStack user data endpoint: #{e.message}")
        end

        logger.info("CloudStack user data: #{raw_user_data.inspect}")
        @user_data = parse_user_data(raw_user_data)
      end

      ##
      # Parses the CloudStack user data.
      #
      # @param [String] raw_user_data Raw CloudStack user data
      # @return [Hash] CloudStack user data
      def parse_user_data(raw_user_data)
        begin
          user_data = Yajl::Parser.parse(raw_user_data)
        rescue Yajl::ParseError => e
          raise LoadSettingsError, "Cannot parse user data #{raw_user_data.inspect}: #{e.message}"
        end

        unless user_data.is_a?(Hash)
          raise LoadSettingsError, "Invalid user data format, Hash expected, got #{user_data.class}: #{user_data}"
        end

        user_data
      end

      ##
      # Sends GET request to an specified URI.
      #
      # @param [String] uri URI to request
      # @return [String] Response body
      def get_uri(uri)
        client = HTTPClient.new
        client.send_timeout = HTTP_API_TIMEOUT
        client.receive_timeout = HTTP_API_TIMEOUT
        client.connect_timeout = HTTP_CONNECT_TIMEOUT

        headers = {"Accept" => "application/json"}
        response = client.get(uri, {}, headers)
        unless response.status == 200
          raise LoadSettingsError, "Endpoint #{uri} returned HTTP #{response.status}"
        end

        response.body
      rescue HTTPClient::TimeoutError
        raise LoadSettingsError, "Timed out reading endpoint #{uri}"
      rescue HTTPClient::BadResponseError => e
        raise LoadSettingsError, "Received bad HTTP response from endpoint #{uri}: #{e.inspect}"
      rescue URI::Error, SocketError, Errno::ECONNREFUSED, SystemCallError => e
        raise LoadSettingsError, "Error requesting endpoint #{uri}: #{e.inspect}"
      end
    end
  end
end