# coding: utf-8 # rubocop:disable Metrics/LineLength require 'rainbow' require 'json' require 'clamp' require 'set' require 'logger' require 'gaptool_client/api' require 'gaptool_client/helpers' require 'gaptool_client/ssh' module Gaptool class InitCommand < Clamp::Command option ['-r', '--role'], 'ROLE', 'Resource name to initilize', required: true option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production', required: true option ['-z', '--zone'], 'ZONE', 'AWS availability zone to put node in', default: 'us-west-2c' option ['-t', '--type'], 'TYPE', 'Type of instance, e.g. m1.large', required: true option ['-s', '--security-group'], 'SECURITY_GROUP', 'Security group name. Defaults to $role-$environment' option ['-a', '--ami'], 'AMI_ID', 'Use a specific AMI for the instance (i.e. ami-xxxxxxx)' option ['-C', '--chef-repo'], 'GITURL', 'git url for the chef repository' option ['-b', '--chef-branch'], 'BRANCH', 'branch of the chef repository to use' option(['-R', '--chef_runlist'], 'RECIPE|ROLE', 'override chef run_list. recipe[cb::recipe] or role[myrole]. Can be specified multiple times', multivalued: true, attribute_name: 'chef_runlist') option ['--no-terminate'], :flag, 'Add terminate protection' def execute no_terminate = no_terminate? ? true : nil res = Gaptool::API.client.addnode(zone, type, role, environment, nil, security_group, ami, chef_repo, chef_branch, chef_runlist, no_terminate) Gaptool::Helpers.error(res['message']) if res['result'] == 'error' res.merge( 'role' => role, 'environment' => environment ) res.delete('secret') Gaptool::Helpers.info([res], false, false) end end class TerminateCommand < Clamp::Command option ['-i', '--instance'], 'INSTANCE', 'Instance ID, e.g. i-12345678', required: true option ['-z', '--zone'], 'ZONE', 'AWS region of the node (deprecated/ignored)' option ['-r', '--role'], 'ROLE', 'Resource name to initilize', required: true option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production', required: true def execute node = Gaptool::API.client.getonenode(instance) nodes = [node] Gaptool::Helpers.info(nodes, false, false) zone = node['zone'][0..-2] if node['environment'] != environment || node['role'] != role Gaptool::Helpers.error("'#{node['role']}-#{node['environment']}' do not match provided value (#{role}-#{environment})") end if node['terminable'] == false puts Rainbow('"terminate" command is disabled for this instance').yellow puts 'To terminate the instance, set it as terminable first running:' puts Rainbow("gt set -k terminable -v true -i #{instance}").cyan puts Gaptool::Helpers.error("Cannot terminate instance #{instance}", code: 2) end print Rainbow('Terminate instance? [type yes to confirm]: ').green res = $stdin.gets.chomp return 0 unless res.downcase == 'yes' puts "Terminating instance #{node['role']}:#{node['environment']}:#{node['instance']} in region #{zone}" begin res = Gaptool::API.client.terminatenode(instance, zone) rescue => e Gaptool::Helpers.error("Cannot terminate instance: #{e}") end Gaptool::Helpers.error("Cannot terminate instance: #{res['message']}") \ if res['result'] == 'error' puts Rainbow("Successfully terminated instance #{instance}").green end end class RuncmdCommand < Clamp::Command option ['-r', '--role'], 'ROLE', 'Instance role' option ['-e', '--environment'], 'ENVIRONMENT', 'Instance environment' option ['-i', '--instance'], 'INSTANCE', 'Instance id (i-xxxxxxxx)' option ['-s', '--serial'], :flag, 'Run command serially. Order of execution is unknown.' option ['-x', '--exclude-hidden'], :flag, 'Exclude hidden hosts' option ['-c', '--continue-on-errors'], :flag, 'Continue execution even if one or more hosts fail' option ['-B', '--batch-size'], 'SIZE', "How many hosts to run in parallel (defaults to #{Gaptool::SSH::BATCH_SIZE})", default: Gaptool::SSH::BATCH_SIZE parameter 'COMMAND ...', 'Command to run', attribute_name: :commands def execute params = exclude_hidden? ? {} : { hidden: true } nodes = Gaptool::API.query_nodes(params.merge(instance: instance, role: role, environment: environment)) res = Gaptool::SSH.exec(nodes, [commands.join(' ')], serial: serial?, continue_on_errors: continue_on_errors?, batch_size: batch_size) exit res end end class RunScriptCommand < Clamp::Command option ['-r', '--role'], 'ROLE', 'Instance role' option ['-e', '--environment'], 'ENVIRONMENT', 'Instance environment' option ['-i', '--instance'], 'INSTANCE', 'Instance id (i-xxxxxxxx)' option ['-s', '--serial'], :flag, 'Run command serially. Order of execution is unknown.' option ['-x', '--exclude-hidden'], :flag, 'Exclude hidden hosts' option ['-c', '--continue-on-errors'], :flag, 'Continue execution even if one or more hosts fail' option ['-B', '--batch-size'], 'SIZE', "How many hosts to run in parallel (defaults to #{Gaptool::SSH::BATCH_SIZE})", default: Gaptool::SSH::BATCH_SIZE option ['-v', '--verbose'], :flag, 'Enable verbose logging;' option ['-D', '--debug'], :flag, 'Enable debug logging;' parameter 'SCRIPT', 'Ruby script to run' def execute params = exclude_hidden? ? {} : { hidden: true } nodes = Gaptool::API.query_nodes(params.merge(instance: instance, role: role, environment: environment)) log_level = if debug? Logger::DEBUG elsif verbose? Logger::INFO end command = "echo 'done'" unless log_level sc = File.read(script) nodes = nodes.map { |x| x.merge(script: sc, file: script, verbose: log_level.nil?) } hook = proc do |node| node.handler.on_data('', :stdout, "Executing #{node[:file]}", '') if node[:verbose] eval(node[:script]) end res = Gaptool::SSH.exec(nodes, [command], pre_hooks: [hook], serial: serial?, continue_on_errors: continue_on_errors?, log_level: log_level, batch_size: batch_size) exit res end end class SSHConfigCommand < Clamp::Command option ['-r', '--remove'], :flag, 'Remove ssh configuration' def execute config, content = Gaptool::SSH.config if remove? data = '' else puts Rainbow('Getting nodes from API').green data = [Gaptool::SSH::START_MARKER, '# to remove this block, run gt ssh-config --remove'] Gaptool::API.client.getallnodes(hidden: true).sort_by { |n| n['instance'] }.each do |node| host = Gaptool::API.get_host(node) puts " - #{Rainbow(host).blue}" data << Gaptool::SSH.config_for(node) end data << Gaptool::SSH::STOP_MARKER data = data.join("\n") end if Regexp.new(Gaptool::SSH::START_MARKER).match(content) content.gsub!(/#{Gaptool::SSH::START_MARKER}.*?#{Gaptool::SSH::STOP_MARKER}/m, data) elsif !remove? content = content + "\n" + data end File.open(config, 'w') { |f| f.write(content) } end end class SSHCommand < Clamp::Command option ['-r', '--role'], 'ROLE', 'Instance role' option ['-e', '--environment'], 'ENVIRONMENT', 'Instance environment' option ['-i', '--instance'], 'INSTANCE', 'Instance id (i-xxxxxxxx)' option ['-f', '--first'], :flag, 'Just connect to first available instance' option ['-t', '--tmux'], :flag, 'No-op, DEPRECATED' def execute puts Rainbow('tmux support has been removed').yellow if tmux? nodes = Gaptool::API.query_nodes(hidden: true, instance: instance, environment: environment, role: role) if first? || (nodes.length == 1 && !instance) puts Rainbow('No instance specified, but only one instance in cluster or first forced').green node = nodes.first elsif !instance nodes.each_index do |i| puts "#{i}: #{nodes[i]['instance']}" end print Rainbow('Select a node: ').cyan node = nodes[$stdin.gets.chomp.to_i] error 'Invalid selection' if node.nil? else node = nodes.first end Gaptool::SSH.update_config_for(node) system "ssh #{node['instance']}" end end class InfoCommand < Clamp::Command option ['-r', '--role'], 'ROLE', 'Role name, e.g. frontend' option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production' option ['-i', '--instance'], 'INSTANCE', 'Node instance, leave blank to query avilable nodes' option ['-s', '--short'], :flag, 'Only show summary for hosts' option ['-p', '--parseable'], :flag, 'Display in non-pretty parseable JSON' option ['-g', '--grepable'], :flag, 'Display in non-pretty grep-friendly text' option ['-H', '--hidden'], :flag, 'Display hidden hosts' def execute params = hidden? ? { hidden: true } : {} nodes = Gaptool::API.query_nodes(params.merge(instance: instance, role: role, environment: environment)) Gaptool::Helpers.info(nodes, parseable?, grepable?, short?) end end class SetCommand < Clamp::Command option ['-i', '--instance'], 'INSTANCE', 'Node instance, required', required: true option ['-p', '--parseable'], :flag, 'Display in non-pretty parseable JSON' option ['-g', '--grepable'], :flag, 'Display in non-pretty grep-friendly text' option ['-k', '--parameter'], 'NAME', 'Set parameter for the node', required: true, multivalued: true option ['-v', '--value'], 'VALUE', 'Value for parameter', required: true, multivalued: true def convert_bool(v) case v.downcase when 'true' true when 'false' false else v end end def execute if parameter_list.length != value_list.length puts Rainbow('parameter and value length mismatch').red end params = Hash[parameter_list.each_with_index.map { |p, i| [p, convert_bool(value_list[i])] }] Gaptool::Helpers.info([Gaptool::API.client.setparameters(instance, params)], parseable?, grepable?) end end class ChefrunCommand < Clamp::Command option ['-r', '--role'], 'ROLE', 'Role name to ssh to' option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production' option ['-i', '--instance'], 'INSTANCE', 'Instance ID, e.g. i-12345678' option ['-H', '--hidden'], :flag, 'Include hidden hosts' option(['-A', '--attribute'], 'ATTRIBUTE', 'Pass one or more parameters to the deploy recipe in recipe.attr=value format', multivalued: true) option(['-b', '--chef-branch'], 'BRANCH', 'branch of the chef repository to use (defaults to last branch used during init/chefrun)', default: nil) option ['-s', '--serial'], :flag, 'Run command serially. Order of execution is unknown.' option ['-W', '--whyrun'], :flag, 'Whyrun, like dry-run but different.' option ['-c', '--continue-on-errors'], :flag, 'Continue execution even if one or more hosts fail' option ['-B', '--batch-size'], 'SIZE', "How many hosts to run in parallel (defaults to #{Gaptool::SSH::BATCH_SIZE})", default: Gaptool::SSH::BATCH_SIZE def execute attrs = Gaptool::Helpers.split_attrs(attribute_list) nodes = Gaptool::API.query_nodes(hidden: hidden? ? true : nil, role: role, instance: instance, environment: environment) nodes = nodes.map do |x| x['whyrun'] = whyrun? x['attrs'] = { 'chef_branch' => chef_branch }.merge(attrs) x end pre_hook = proc do |node| cl = Gaptool::API.new json = cl.getnodeattrs(node['instance'], node['attrs']) json['run_list'] ||= node['chef_runlist'] || ['role[base]'] git = 'sudo -u admin git' pull = "#{git} fetch --all; #{git} reset --hard origin/`#{git} rev-parse --abbrev-ref HEAD`" wopts = node['whyrun'] ? ' -W ' : '' unless node['attrs']['chef_branch'].nil? json['chefbranch'] = node['attrs']['chef_branch'] pull = "#{git} checkout -fB #{node['attrs']['chef_branch']}; #{git} fetch --all; #{git} reset --hard origin/#{node['attrs']['chef_branch']}" end upload!(StringIO.new(json.to_json), '/tmp/chef.json') script = <<-EOS #!/bin/bash set -e cd /var/data/admin/ops #{pull} sudo chef-solo -c /var/data/admin/ops/cookbooks/solo.rb -j /tmp/chef.json -E #{node['environment']}#{wopts} rm -f /tmp/chef.json EOS upload!(StringIO.new(script), '/tmp/chef.sh') end res = Gaptool::SSH.exec( nodes, ['chmod +x /tmp/chef.sh', '/tmp/chef.sh', 'rm -f /tmp/chef.sh'], pre_hooks: [pre_hook], serial: serial?, continue_on_errors: continue_on_errors?, batch_size: batch_size ) exit res end end class DeployCommand < Clamp::Command option(['-a', '--app'], 'APP', 'Application(s) to deploy (can be set multiple times)', required: true, multivalued: true) option ['-m', '--migrate'], :flag, 'Toggle running migrations' option ['-e', '--environment'], 'ENVIRONMENT', 'Which environment, e.g. production', required: true option ['-b', '--branch'], 'BRANCH', 'Git branch to deploy, default is master' option ['-r', '--rollback'], :flag, 'Toggle this to rollback last deploy' option ['-i', '--instance'], 'INSTANCE', 'Instance ID, e.g. i-12345678. If set, all applications MUST be hosted on this node.' option ['-A', '--attribute'], 'ATTRIBUTE', 'Pass one or more parameters to the deploy recipe in recipe.attr=value format', multivalued: true option ['-H', '--hidden'], :flag, 'Display hidden hosts' option ['-s', '--serial'], :flag, 'Run command serially. Order of execution is unknown.' option ['-c', '--continue-on-errors'], :flag, 'Continue execution even if one or more hosts fail' option ['-B', '--batch-size'], 'SIZE', "How many hosts to run in parallel (defaults to #{Gaptool::SSH::BATCH_SIZE})", default: Gaptool::SSH::BATCH_SIZE option ['-v', '--verbose'], :flag, 'More verbose output' def execute attrs = Gaptool::Helpers.split_attrs(attribute_list) if instance n = Gaptool::API.client.getonenode(instance) if n['environment'] != environment Gaptool::Helpers.error "Instance #{instance} is not in environment #{environment}" else app_list.each do |app| Gaptool::Helpers.error "Instance #{instance} does not host #{app} in env #{environment}" \ unless n['apps'].include?(app) end end nodes = [n] else params = hidden? ? { hidden: true } : {} nodes = [] app_list.each do |app| nodes.concat(Gaptool::API.client.getappnodes(app, environment, params)) end end # dedup nodes seen = Set.new app_set = Set.new(app_list) nodes = nodes.select do |x| res = !seen.include?(x['instance']) seen << x['instance'] res end nodes = nodes.map do |x| x['verbose'] = verbose? x['attrs'] = { 'deploy_apps' => (Set.new(x['apps']) & app_set).to_a, 'rollback' => rollback?, 'branch' => branch, 'migrate' => migrate? }.merge(attrs) x end pre_hook = proc do |node| client = Gaptool::API.new json = client.getnodeattrs(node['instance'], node['attrs']) json['run_list'] = node['chef_runlist'] || ['recipe[deploy]'] host = "#{node['role']}:#{node['environment']}:#{node['instance']}" puts "#{Rainbow('Deploying apps').cyan} '" + \ Rainbow(json['deploy_apps'].join(' ')).green + \ "' #{Rainbow('on').cyan} " + \ Rainbow(host).green puts "#{Rainbow(host).green}: #{JSON.pretty_generate(json)}" if node['verbose'] upload!(StringIO.new(json.to_json), '/tmp/chef.json') script = <<-EOS #!/bin/bash set -e cd /var/data/admin/ops sudo -u admin git pull sudo chef-solo -c /var/data/admin/ops/cookbooks/solo.rb -j /tmp/chef.json -E #{node['environment']} rm -f /tmp/chef.json EOS upload!(StringIO.new(script), '/tmp/deploy.sh') end res = Gaptool::SSH.exec( nodes, ['chmod +x /tmp/deploy.sh', '/tmp/deploy.sh', 'rm -f /tmp/deploy.sh'], pre_hooks: [pre_hook], serial: serial?, continue_on_errors: continue_on_errors?, batch_size: batch_size ) exit res end end class RehashCommand < Clamp::Command option ['-y', '--yes'], :flag, 'YES I REALLY WANT TO DO THIS' def execute if yes? puts Gaptool::API.client.rehash else puts "You need to run this with -y\nIf you don't know what this does or aren't sure, DO NOT RUN IT\nThis will regenerate all host metadata on gaptool-server\nand can break in-progress operations." end end end class VersionCommand < Clamp::Command option ['-r', '--remote'], :flag, 'Include remote API version' def execute version = File.read(File.realpath(File.join(File.dirname(__FILE__), '..', '..', 'VERSION'))).strip puts "gaptool-client #{version} using gaptool-api #{Gaptool::API.client.version}" return 0 unless remote? vinfo = Gaptool::API.client.api_version puts "gaptool-server #{vinfo['server_version']}" end end class ScpCommand < Clamp::Command option ['-r', '--role'], 'ROLE', 'Instance role' option ['-e', '--environment'], 'ENVIRONMENT', 'Instance environment' option ['-i', '--instance'], 'INSTANCE', 'Instance id (i-xxxxxxxx)' option ['-s', '--serial'], :flag, 'Run command serially. Order of execution is unknown.' option ['-x', '--exclude-hidden'], :flag, 'Exclude hidden hosts' option ['-c', '--continue-on-errors'], :flag, 'Continue execution even if one or more hosts fail' option ['-B', '--batch-size'], 'SIZE', "How many hosts to run in parallel (defaults to #{Gaptool::SSH::BATCH_SIZE})", default: Gaptool::SSH::BATCH_SIZE parameter 'SRC', 'Source file spec', attribute_name: :src parameter '[DEST]', 'Dest file spec.', attribute_name: :dest def execute if src.start_with?(':') # from remote to local signal_usage_error('SRC and DEST cannot be both remote') \ if dest && dest.start_with?(':') source = src[1..-1] destf = dest ? dest : File.join(Dir.pwd, File.basename(source)) destd = File.dirname(destf) Gaptool::Helpers.error("Cannot write to #{destd}") \ unless File.writable?(destd) pre_hook = proc do |node| download! node['source'], "#{node['destf']}.#{node['instance']}" end else # from local to remote signal_usage_error('SRC and DEST cannot be both local') \ if dest && !dest.start_with?(':') begin source = File.realpath(src) rescue => e Gaptool::Helpers.error(e) end destf = dest ? dest[1..-1] : File.basename(src) if destf.end_with?('/') destd = destf destf = File.join(destf, File.basename(src)) else destd = File.dirname(destf) end pre_hook = proc do |node| if test("[ -w #{node['destd']} ]") upload! node['source'], node['destf'] else fail "No such directory #{node['destd']}" \ unless test("[ -d #{node['destd']} ]") tmp = File.join('/tmp', File.basename(node['destf'])) upload! node['source'], tmp execute("sudo mv #{tmp} #{node['destf']}") end end end params = exclude_hidden? ? {} : { hidden: true } nodes = Gaptool::API.query_nodes(params.merge(instance: instance, role: role, environment: environment)) nodes = nodes.map { |x| x.merge('source' => source, 'destd' => destd, 'destf' => destf) } res = Gaptool::SSH.exec( nodes, [], pre_hooks: [pre_hook], serial: serial?, continue_on_errors: continue_on_errors?, batch_size: batch_size, log_level: Logger::INFO ) exit res end end end