module Services # # Setup ETCD connection via chef or plain host # Also stores that aconnection in Services.connection # Most other classes require this to be setup # class Connection require 'openssl' attr_reader :node, :run_context, :client, :host, :port, :ssl_verify if defined?(Chef) == 'constant' && Chef.class == Class if Chef::Version.new(Chef::VERSION) <= Chef::Version.new('11.0.0') include ::Chef::Mixin::Language else include ::Chef::DSL::DataQuery end end # # Initialize etcd client # # You should pass either a run_context or explicit host/port arguments # the run_context will take prescedence # # @param [Hash] options # @option args [Chef::RunContext] :run_context (nil) # The chef run context to find things in # @option args [String] :host (nil) The host address to connect too # @option args [String] :port (4001) The etcd port to connect too def initialize(args) @run_context = args.fetch(:run_context, nil) Services.run_context = args[:run_context] @node = args[:run_context].node if run_context @host = args[:host] @port = args[:port] || 4001 @redirect = args[:redirect] || false @ssl_verify = args[:verify] || OpenSSL::SSL::VERIFY_NONE validate load_gem Services.connection = get_connection(find_servers) end private # # Validate args passeed in on initialize # # We require run_context OR host to function # def validate unless run_context || host fail ArgumentError, 'Must provide a run_context OR host to initialize' end end # # Lazily Load the gem requirement so we can run inside chef # # If @run_context exists it will use that to install the gem via # Chefs chef_gem resource # def load_gem require 'etcd' rescue LoadError if run_context Chef::Log.info 'etcd gem not found. attempting to install' g = Chef::Resource::ChefGem.new 'etcd', run_context g.version '0.0.6' run_context.resource_collection.insert g g.run_action :install require 'etcd' end end # # Find other Etd Servers by looking at node attributes or via Chef Search # def find_servers # need a run_context to find anything in return nil unless run_context # If there are already servers in attribs use those return node[:etcd][:servers] if node.key?(:etcd) && node[:etcd].key?(:servers) # if we have already searched in this run use those return node.run_state[:etcd_servers] if node.run_state.key? :etcd_servers # find nodes and build array of ip's etcd_nodes = search(:node, search_query) servers = etcd_nodes.map { |n| n[:ipaddress] } # store that in the run_state node.run_state[:etcd_servers] = servers end # # Setup proper chef search term for other etcd boxen # # Will search for known recipe in run_list or specified term # def search_query query = "(chef_environment:#{node.chef_environment} " query << 'AND recipes:etcd) ' if node[:etcd][:recipe] query << "OR (chef_environment:#{node.chef_environment} " query << "AND #{node[:etcd][:search_term]})" end query end # # connect to ip/port and store in @@client # If given an arry of servers then try each until we # connect # TODO: refactor # rubocop:disable MethodLength def get_connection(servers = nil) c = nil if servers servers.each do |s| c = try_connect(s) break if c end else c = try_connect host end fail 'Unable to get a valid connection to Etcd' unless c c end # # Try to grab an etcd connection # # @param [String] server () The server to try to connect too # def try_connect(server) c = ::Etcd.client(host: server, port: port, allow_redirect: @redirect) begin c.get '/_etcd/machines' return c rescue puts "ETCD: failed to connect to #{c.host}:#{c.port}" return nil end end end end