# coding: utf-8 require 'logger' require 'rainbow' require 'sshkit' require 'gaptool_client/runner' require 'gaptool_client/host' # rubocop:disable Metrics/LineLength module Gaptool module SSH BATCH_SIZE = 10 START_MARKER = '###### GAPTOOL ######' STOP_MARKER = '###### END GAPTOOL ######' def self.ssh_identity @ssh_id ||= if ENV['GT_SSH_KEY'] File.realpath(File.expand_path(ENV['GT_SSH_KEY'])) end end def self.config_for(node) key = "\n IdentityFile #{ssh_identity}" if ssh_identity host = Gaptool::API.get_host(node) <<-EOF # -- #{node['instance']} Host #{host} #{node['instance']} Hostname #{node['hostname']} User #{ENV['GT_USER']} LogLevel FATAL PreferredAuthentications publickey CheckHostIP no StrictHostKeyChecking no UserKnownHostsFile /dev/null#{key} # -- end #{node['instance']} EOF end def self.config config = File.join(Dir.home, '.ssh', 'config') dir = File.dirname(config) parent = File.join(dir, '..') if File.exist?(config) Gaptool::Helpers.error "#{config}: not writable" unless File.writable?(config) content = File.read(config) File.open("#{config}.bck", 'w') { |f| f.write(content) } else if !File.exist?(dir) Gaptool::Helpers.error "Home directory #{parent} does not exists"\ unless Dir.exist?(parent) Dir.mkdir(dir, 0700) elsif !File.directory?(dir) Gaptool::Helpers.error "#{dir}: not a directory" end content = '' end [config, content] end def self.update_config_for(nodes, verbose = true) nodes = [nodes] unless nodes.is_a?(Array) config, content = Gaptool::SSH.config nodes.each do |node| snip = Gaptool::SSH.config_for(node) mark = "# -- #{node['instance']}" emark = "# -- end #{node['instance']}" if Regexp.new(mark).match(content) puts Rainbow("Updating ssh config for #{Gaptool::API.get_host(node)}").green if verbose content.gsub!(/#{mark}.*?#{emark}/m, snip.strip) elsif Regexp.new(STOP_MARKER).match(content) puts Rainbow("Adding ssh config for #{Gaptool::API.get_host(node)}").green if verbose content.gsub!(/#{STOP_MARKER}/m, snip + "\n" + STOP_MARKER) else puts Rainbow('No gt ssh config found: please run gt ssh-config').yellow puts Rainbow("Adding ssh config for #{Gaptool::API.get_host(node)}").green if verbose content = <<-EOF #{content} #{START_MARKER} #{snip} #{STOP_MARKER} EOF end end File.open(config, 'w') { |f| f.write(content) } end def self.configure_sshkit(opts = {}) SSHKit.config.output_verbosity = opts[:log_level] || Logger::WARN opts = { forward_agent: true, global_known_hosts_file: '/dev/null', keys_only: true, port: 22, user: ENV['GT_USER'] } opts[:keys] = [ssh_identity] if ssh_identity SSHKit::Backend::Netssh.configure do |ssh| ssh.connection_timeout = 30 ssh.ssh_options = opts end end def self.exec(nodes, commands, opts = {}) logger = SSHKit::Formatter::Pretty.new(STDERR) serial = opts[:serial] || nodes.length == 1 if opts[:update_ssh_config].nil? || opts[:update_ssh_config] nodes.each { |n| Gaptool::SSH.update_config_for(n, false) } end pre = opts[:pre_hooks] || [] post = opts[:post_hooks] || [] Gaptool::SSH.configure_sshkit(log_level: opts[:log_level]) opts = { limit: opts[:batch_size].to_i || BATCH_SIZE, on_errors: opts[:continue_on_errors] ? :continue : :exit, wait: opts[:wait] || 0 } runner_cls = if serial SSHKit::Runner::SafeSequential else SSHKit::Runner::SafeParallel end hosts = nodes.map { |n| Gaptool::Host.new(n) } runner = runner_cls.new(hosts, opts) do |host| pre.each { |h| instance_exec(host, &h) } commands.each do |cmd| next if cmd.nil? || cmd.empty? execute(:bash, "-l -c '#{cmd}'", interaction_handler: host.handler) end post.each { |h| instance_exec(host, &h) } end res = runner.execute unless res logger.error("#{runner.failed.length}/#{nodes.length} hosts failed") runner.failed.each do |desc| host = desc[:host] error = desc[:error] logger.error("#{host.name}> #{error.message}") end return runner.failed.length + 100 end 0 end end end