cloud.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. #!/usr/bin/env ruby
  2. require 'thor'
  3. require 'json'
  4. require 'yaml'
  5. require 'securerandom'
  6. require 'fileutils'
  7. require 'parseconfig'
  8. SCRIPT_DIR = File.expand_path(File.dirname(__FILE__))
  9. module OpenShift
  10. module Ops
  11. # WARNING: we do not currently support environments with hyphens in the name
  12. SUPPORTED_ENVS = ['prod','stg','int','tint','kint','test']
  13. class GceHelper
  14. def self.list_hosts()
  15. cmd = "#{SCRIPT_DIR}/inventory/gce/gce.py --list"
  16. hosts = %x[#{cmd} 2>&1]
  17. raise "Error: failed to list hosts\n#{hosts}" unless $?.exitstatus == 0
  18. return JSON.parse(hosts)
  19. end
  20. def self.get_host_details(host)
  21. cmd = "#{SCRIPT_DIR}/inventory/gce/gce.py --host #{host}"
  22. details = %x[#{cmd} 2>&1]
  23. raise "Error: failed to get host details\n#{details}" unless $?.exitstatus == 0
  24. retval = JSON.parse(details)
  25. # Convert OpenShift specific tags to entries
  26. retval['gce_tags'].each do |tag|
  27. if tag =~ /\Ahost-type-([\w\d-]+)\z/
  28. retval['host-type'] = $1
  29. end
  30. if tag =~ /\Aenv-([\w\d]+)\z/
  31. retval['env'] = $1
  32. end
  33. end
  34. return retval
  35. end
  36. def self.generate_env_tag(env)
  37. return "env-#{env}"
  38. end
  39. def self.generate_env_tag_name(env)
  40. return "tag_#{generate_env_tag(env)}"
  41. end
  42. def self.generate_host_type_tag(host_type)
  43. return "host-type-#{host_type}"
  44. end
  45. def self.generate_host_type_tag_name(host_type)
  46. return "tag_#{generate_host_type_tag(host_type)}"
  47. end
  48. def self.generate_env_host_type_tag(env, host_type)
  49. return "env-host-type-#{env}-#{host_type}"
  50. end
  51. def self.generate_env_host_type_tag_name(env, host_type)
  52. return "tag_#{generate_env_host_type_tag(env, host_type)}"
  53. end
  54. end
  55. class LaunchHelper
  56. def self.expand_name(name)
  57. return [name] unless name =~ /^([a-zA-Z0-9\-]+)\{(\d+)-(\d+)\}$/
  58. # Regex matched, so grab the values
  59. start_num = $2
  60. end_num = $3
  61. retval = []
  62. start_num.upto(end_num) do |i|
  63. retval << "#{$1}#{i}"
  64. end
  65. return retval
  66. end
  67. def self.get_gce_host_types()
  68. return Dir.glob("#{SCRIPT_DIR}/playbooks/gce/*").map { |d| File.basename(d) }
  69. end
  70. end
  71. class AnsibleHelper
  72. attr_accessor :inventory, :extra_vars, :verbosity, :pipelining
  73. def initialize(extra_vars={}, inventory=nil)
  74. @extra_vars = extra_vars
  75. @verbosity = '-vvvv'
  76. @pipelining = true
  77. end
  78. def run_playbook(playbook)
  79. @inventory = 'inventory/hosts' if @inventory.nil?
  80. # This is used instead of passing in the json on the cli to avoid quoting problems
  81. tmpfile = Tempfile.new('extra_vars')
  82. tmpfile.write(@extra_vars.to_json)
  83. tmpfile.sync()
  84. tmpfile.close()
  85. cmds = []
  86. #cmds << 'set -x'
  87. cmds << %Q[export ANSIBLE_FILTER_PLUGINS="#{Dir.pwd}/filter_plugins"]
  88. # We need this for launching instances, otherwise conflicting keys and what not kill it
  89. cmds << %q[export ANSIBLE_TRANSPORT="ssh"]
  90. cmds << %q[export ANSIBLE_SSH_ARGS="-o ForwardAgent=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"]
  91. # We need pipelining off so that we can do sudo to enable the root account
  92. cmds << %Q[export ANSIBLE_SSH_PIPELINING='#{@pipelining.to_s}']
  93. ssh_key_arg = %q[--private-key=~/.ssh/mmcgrath_libra] if File.file?(ENV['HOME']+'/.ssh/mmcgrath_libra.pem')
  94. cmds << %Q[time -p ansible-playbook -i #{@inventory} #{@verbosity} #{playbook} #{ssh_key_arg} --extra-vars '@#{tmpfile.path}']
  95. cmd = cmds.join(' ; ')
  96. unless system(cmd)
  97. puts %Q[Following command failed with exit code: #{$?.exitstatus}\n#{cmd}]
  98. puts %Q[extra_vars: #{@extra_vars.to_json}]
  99. end
  100. tmpfile.unlink
  101. end
  102. def merge_extra_vars_file(file)
  103. vars = YAML.load_file(file)
  104. @extra_vars.merge!(vars)
  105. end
  106. def self.for_gce
  107. ah = AnsibleHelper.new
  108. # GCE specific configs
  109. gce_ini = "#{SCRIPT_DIR}/inventory/gce/gce.ini"
  110. config = ParseConfig.new(gce_ini)
  111. if config['gce']['gce_project_id'].to_s.empty?
  112. raise %Q['gce_project_id' not set in #{gce_ini}]
  113. end
  114. ah.extra_vars['gce_project_id'] = config['gce']['gce_project_id']
  115. if config['gce']['gce_service_account_pem_file_path'].to_s.empty?
  116. raise %Q['gce_service_account_pem_file_path' not set in #{gce_ini}]
  117. end
  118. ah.extra_vars['gce_pem_file'] = config['gce']['gce_service_account_pem_file_path']
  119. if config['gce']['gce_service_account_email_address'].to_s.empty?
  120. raise %Q['gce_service_account_email_address' not set in #{gce_ini}]
  121. end
  122. ah.extra_vars['gce_service_account_email'] = config['gce']['gce_service_account_email_address']
  123. ah.inventory = 'inventory/gce/gce.py'
  124. return ah
  125. end
  126. end
  127. class GceCommand < Thor
  128. option :type, :required => true, :enum => LaunchHelper.get_gce_host_types,
  129. :desc => 'The host type of the new instances.'
  130. option :env, :required => true, :aliases => '-e', :enum => OpenShift::Ops::SUPPORTED_ENVS,
  131. :desc => 'The environment of the new instances.'
  132. option :count, :default => 1, :aliases => '-c', :type => :numeric,
  133. :desc => 'The number of instances to create'
  134. option :tag, :type => :array,
  135. :desc => 'The tag(s) to add to the new instances. Allowed characters are letters, numbers, and hyphens.'
  136. desc "launch", "Launches instances."
  137. def launch()
  138. # Expand all of the instance names so that we have a complete array
  139. names = []
  140. options[:count].times { names << "#{options[:env]}-#{options[:type]}-#{SecureRandom.hex(5)}" }
  141. ah = AnsibleHelper.for_gce()
  142. # GCE specific configs
  143. ah.extra_vars['oo_new_inst_names'] = names
  144. ah.extra_vars['oo_new_inst_tags'] = options[:tag]
  145. ah.extra_vars['oo_env'] = options[:env]
  146. # Add a created by tag
  147. ah.extra_vars['oo_new_inst_tags'] = [] if ah.extra_vars['oo_new_inst_tags'].nil?
  148. ah.extra_vars['oo_new_inst_tags'] << "created-by-#{ENV['USER']}"
  149. ah.extra_vars['oo_new_inst_tags'] << GceHelper.generate_env_tag(options[:env])
  150. ah.extra_vars['oo_new_inst_tags'] << GceHelper.generate_host_type_tag(options[:type])
  151. ah.extra_vars['oo_new_inst_tags'] << GceHelper.generate_env_host_type_tag(options[:env], options[:type])
  152. puts
  153. puts 'Creating instance(s) in GCE...'
  154. puts
  155. puts %q[ .---- Spurious warning "It is unnecessary to use '{{' in loops" (ansible bug 6407) ----.]
  156. puts %q[ V V]
  157. ah.run_playbook("playbooks/gce/#{options[:type]}/launch.yml")
  158. end
  159. option :name, :required => false, :type => :string,
  160. :desc => 'The name of the instance to configure.'
  161. option :env, :required => false, :aliases => '-e', :enum => OpenShift::Ops::SUPPORTED_ENVS,
  162. :desc => 'The environment of the new instances.'
  163. option :type, :required => false, :enum => LaunchHelper.get_gce_host_types,
  164. :desc => 'The type of the instances to configure.'
  165. desc "config", 'Configures instances.'
  166. def config()
  167. ah = AnsibleHelper.for_gce()
  168. abort 'Error: you can\'t specify both --name and --type' unless options[:type].nil? || options[:name].nil?
  169. abort 'Error: you can\'t specify both --name and --env' unless options[:env].nil? || options[:name].nil?
  170. host_type = nil
  171. if options[:name]
  172. details = GceHelper.get_host_details(options[:name])
  173. ah.extra_vars['oo_host_group_exp'] = options[:name]
  174. ah.extra_vars['oo_env'] = details['env']
  175. host_type = details['host-type']
  176. elsif options[:type] && options[:env]
  177. oo_env_host_type_tag = GceHelper.generate_env_host_type_tag_name(options[:env], options[:type])
  178. ah.extra_vars['oo_host_group_exp'] = "groups['#{oo_env_host_type_tag}']"
  179. ah.extra_vars['oo_env'] = options[:env]
  180. host_type = options[:type]
  181. else
  182. abort 'Error: you need to specify either --name or (--type and --env)'
  183. end
  184. puts
  185. puts "Configuring #{options[:type]} instance(s) in GCE..."
  186. puts
  187. puts " .---- Disregard this (ansible bug 6407) ----."
  188. puts " V V"
  189. ah.run_playbook("playbooks/gce/#{host_type}/config.yml")
  190. end
  191. desc "list", "Lists instances."
  192. def list()
  193. hosts = GceHelper.list_hosts()
  194. data = {}
  195. hosts.each do |key,value|
  196. value.each { |h| (data[h] ||= []) << key }
  197. end
  198. puts
  199. puts "Instances"
  200. puts "---------"
  201. data.keys.sort.each { |k| puts " #{k}" }
  202. puts
  203. end
  204. option :file, :required => true, :type => :string,
  205. :desc => 'The name of the file to copy.'
  206. option :dest, :required => false, :type => :string,
  207. :desc => 'A relative path where files are written to.'
  208. desc "scp_from", "scp files from an instance"
  209. def scp_from(*ssh_ops, host)
  210. if host =~ /^([\w\d_.-]+)@([\w\d-_.]+)$/
  211. user = $1
  212. host = $2
  213. end
  214. path_to_file = options['file']
  215. dest = options['dest']
  216. details = GceHelper.get_host_details(host)
  217. abort "\nError: Instance [#{host}] is not RUNNING\n\n" unless details['gce_status'] == 'RUNNING'
  218. cmd = "scp #{ssh_ops.join(' ')}"
  219. if user.nil?
  220. cmd += " "
  221. else
  222. cmd += " #{user}@"
  223. end
  224. if dest.nil?
  225. download = File.join(Dir.pwd, 'download')
  226. FileUtils.mkdir_p(download) unless File.exists?(download)
  227. cmd += "#{details['gce_public_ip']}:#{path_to_file} download/"
  228. else
  229. cmd += "#{details['gce_public_ip']}:#{path_to_file} #{File.expand_path(dest)}"
  230. end
  231. exec(cmd)
  232. end
  233. desc "ssh", "Ssh to an instance"
  234. def ssh(*ssh_ops, host)
  235. puts host
  236. if host =~ /^([\w\d_.-]+)@([\w\d-_.]+)/
  237. user = $1
  238. host = $2
  239. end
  240. puts "user=#{user}"
  241. puts "host=#{host}"
  242. details = GceHelper.get_host_details(host)
  243. abort "\nError: Instance [#{host}] is not RUNNING\n\n" unless details['gce_status'] == 'RUNNING'
  244. cmd = "ssh #{ssh_ops.join(' ')}"
  245. if user.nil?
  246. cmd += " "
  247. else
  248. cmd += " #{user}@"
  249. end
  250. cmd += "#{details['gce_public_ip']}"
  251. exec(cmd)
  252. end
  253. option :name, :required => true, :aliases => '-n', :type => :string,
  254. :desc => 'The name of the instance.'
  255. desc 'details', 'Displays details about an instance.'
  256. def details()
  257. name = options[:name]
  258. details = GceHelper.get_host_details(name)
  259. key_size = details.keys.max_by { |k| k.size }.size
  260. header = "Details for #{name}"
  261. puts
  262. puts header
  263. header.size.times { print '-' }
  264. puts
  265. details.each { |k,v| printf("%#{key_size + 2}s: %s\n", k, v) }
  266. puts
  267. end
  268. desc 'types', 'Displays instance types'
  269. def types()
  270. puts
  271. puts "Available Host Types"
  272. puts "--------------------"
  273. LaunchHelper.get_gce_host_types.each { |t| puts " #{t}" }
  274. puts
  275. end
  276. end
  277. class CloudCommand < Thor
  278. desc 'gce', 'Manages Google Compute Engine assets'
  279. subcommand "gce", GceCommand
  280. end
  281. end
  282. end
  283. if __FILE__ == $0
  284. Dir.chdir(SCRIPT_DIR) do
  285. # Kick off thor
  286. OpenShift::Ops::CloudCommand.start(ARGV)
  287. end
  288. end