259 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			259 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| require_relative 'tool'
 | |
| require "lib/ssh/ssh_utils"
 | |
| require "lib/knife/knife_factory"
 | |
| require 'lib/executors/server_executor/server_operation_result'
 | |
| require "db/mongo/models/key"
 | |
| 
 | |
| module Devops
 | |
|   module Model
 | |
|     module ConfigurationManagement
 | |
|       class Chef < Tool
 | |
| 
 | |
|         include Devops::Executor::ServerOperationResultCode
 | |
| 
 | |
|         attr_accessor :bootstrap_template, :chef_env, :use_json_file
 | |
| 
 | |
|         def initialize hash={}
 | |
|           @name = 'chef'
 | |
|           @bootstrap_template = hash['bootstrap_template'] || 'omnibus'
 | |
|           @chef_env = hash.fetch('chef_env')
 | |
|           @use_json_file = hash['use_json_file'] || false
 | |
|         end
 | |
| 
 | |
|         def to_hash
 | |
|           super.merge({
 | |
|             'bootstrap_template' => self.bootstrap_template,
 | |
|             'chef_env' => self.chef_env
 | |
|           })
 | |
|         end
 | |
| 
 | |
|         def knife_instance
 | |
|           @knife_instance ||= KnifeFactory.instance(self.chef_env)
 | |
|         end
 | |
| 
 | |
|         def use_json_file?
 | |
|           self.use_json_file
 | |
|         end
 | |
| 
 | |
|         def check_node_name name
 | |
|           s = knife_instance.chef_node_list.detect {|n| n == name}
 | |
|           raise Devops::Exception::ValidationError.new("Chef node with name '#{name}' already exist") unless s.nil?
 | |
|           s = knife_instance.chef_client_list.detect {|c| c == name}
 | |
|           raise Devops::Exception::ValidationError.new("Chef client with name '#{name}' already exist") unless s.nil?
 | |
|         end
 | |
| 
 | |
|         def prepare options, out
 | |
|           out << "Prepare bootstrap...\n"
 | |
|           out << "Done\n"
 | |
|         end
 | |
| 
 | |
|         # returns bootstrap options
 | |
|         # raises Devops::Exception::BootstrapError
 | |
|         def bootstrap_instance server, attributes, out, options = {}
 | |
|           prepare(options, out)
 | |
|           provider = server.provider_instance
 | |
|           options[:run_list] = provider.run_list if options[:run_list].nil?
 | |
|           server.cm_name = options[:cm_name] || server.name
 | |
|           bootstrap_opts = bootstrap(server, attributes, options, out)
 | |
| 
 | |
|           if check_server_name(server)
 | |
|             out.puts "Server '#{server.id}' has been bootstraped with name '#{server.cm_name}'"
 | |
|           else
 | |
|             raise Devops::Exception::BootstrapError.new("Can not find client or node on chef-server")
 | |
|           end
 | |
|           return {options: bootstrap_opts, info: attributes}
 | |
|         rescue Devops::Exception::BootstrapError => e
 | |
|           raise e
 | |
|         rescue => e
 | |
|           msg = "Bootstrap operation, unexpected error: #{e.message}\n#{e.backtrace.join("\n")}"
 | |
|           out.puts msg
 | |
|           out.flush
 | |
|           DevopsLogger.logger.error(msg)
 | |
|           raise Devops::Exception::BootstrapError.new(msg)
 | |
|         end
 | |
| 
 | |
|         def bootstrap_options server, attributes, options
 | |
|           bootstrap_options = [
 | |
|             "--no-color",
 | |
|             "-x #{server.remote_user}",
 | |
|             "-i #{options[:cert_path]}",
 | |
|             "--json-attributes '#{attributes.to_json}'"
 | |
|           ]
 | |
|           bootstrap_options.push "-N #{server.cm_name}" if server.cm_name
 | |
|           bootstrap_options.push "--sudo" unless server.remote_user == "root"
 | |
|           bootstrap_options.push "-t #{options[:bootstrap_template]}" if options[:bootstrap_template]
 | |
|           bootstrap_options.push "-E #{options[:environment]}" if options[:environment]
 | |
|           rl = options[:run_list]
 | |
|           bootstrap_options.push "-r #{rl.join(",")}" unless rl.nil? or rl.empty?
 | |
|           bootstrap_options.push "-c #{options[:config]}" if options[:config]
 | |
|           bootstrap_options
 | |
|         end
 | |
| 
 | |
|         def bootstrap server, attributes, options, out
 | |
|           out << "\n\nBootstrap...\n"
 | |
|           out.flush
 | |
|           k = Devops::Model::Key.find(server.ssh_key)
 | |
|           options[:cert_path] = k.path
 | |
|           out << "\nBefore bootstrap hooks...\n"
 | |
|           res = self.run_hook(:before_bootstrap, out)
 | |
|           out << "Done\n"
 | |
|           if server.private_ip.nil?
 | |
|             out.puts "Private IP is unset"
 | |
|             out.flush
 | |
|             raise Devops::Exception::BootstrapError.new("Error: Private IP is unset")
 | |
|           end
 | |
|           bootstrap_attrs = attributes.merge({
 | |
|             :provider => server.provider,
 | |
|             :provider_account => server.provider_account,
 | |
|             :devops_host => `hostname`.strip
 | |
|           })
 | |
|           ip = server.private_ip
 | |
|           unless server.public_ip.nil?
 | |
|             ip = server.public_ip
 | |
|             out << "\nPublic IP is present\n"
 | |
|           end
 | |
|           out.flush
 | |
| 
 | |
|           bopts = bootstrap_options(server, bootstrap_attrs, options)
 | |
|           r = knife_instance.knife_bootstrap(out, ip, bopts)
 | |
|           if r == 0
 | |
|             out.puts "Chef node name: #{server.cm_name}\nAfter bootstrap hooks...\n"
 | |
|             res = self.run_hook(:after_bootstrap, out)
 | |
|             msg = "Server with id '#{server.id}' is bootstraped"
 | |
|             out.puts "Done\n#{msg}"
 | |
|             out.flush
 | |
|             DevopsLogger.logger.info msg
 | |
|             return bopts
 | |
|           else
 | |
|             raise Devops::Exception::BootstrapError.new("Can not bootstrap node '#{server.id}', error code: #{r}")
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         def delete_instance server, out
 | |
|           msg = "Removing instance '#{server.cm_name}' from chef server"
 | |
|           out.puts msg
 | |
|           if server.cm_name.nil?
 | |
|             out.puts "There is no specified configuration manager name"
 | |
|             return
 | |
|           end
 | |
|           DevopsLogger.logger.info msg
 | |
|           res = {
 | |
|             :chef_node => knife_instance.chef_node_delete(server.cm_name),
 | |
|             :chef_client => knife_instance.chef_client_delete(server.cm_name)
 | |
|           }
 | |
|           msg = res.values.join("\n")
 | |
|           out.puts msg
 | |
|           DevopsLogger.logger.info msg
 | |
|           server.cm_name = nil
 | |
|           server.update
 | |
|           DevopsLogger.logger.info "Server cm_name has been removed"
 | |
|           k = Devops::Model::Key.find(server.ssh_key)
 | |
|           cert_path = k.path
 | |
|           i = 0
 | |
|           begin
 | |
|             new_name = "/etc/chef.backup_#{Time.now.strftime("%d-%m-%Y_%H.%M.%S")}"
 | |
|             exit_code = Devops::SSH::Utils.run_command_out("/bin/sh -c 'if [[ -d /etc/chef ]]; then mv /etc/chef #{new_name}; else exit 22; fi'", server.public_ip || server.private_ip, server.remote_user, cert_path, out)
 | |
|             if exit_code == 0
 | |
|               out.puts "'/etc/chef' renamed to '#{new_name}'"
 | |
|             elsif exit_code == 22
 | |
|               out.puts "Directory '/etc/chef' does not exists"
 | |
|             else
 | |
|               out.puts "Can not rename /etc/chef directory"
 | |
|             end
 | |
|           rescue Net::SSH::ConnectionTimeout => e
 | |
|             DevopsLogger.logger.error "Unbootstrap error: connection timeout" + e.message
 | |
|           rescue => e
 | |
|             DevopsLogger.logger.error "Unbootstrap error: " + e.message
 | |
|             i += 1
 | |
|             sleep(1)
 | |
|             retry unless i == 5
 | |
|           end
 | |
|           res
 | |
|         end
 | |
| 
 | |
|         def deploy_instance server, deploy_info, out
 | |
|           old_tags_str = nil
 | |
|           new_tags_str = nil
 | |
|           if deploy_info.key(:tags)
 | |
|             old_tags_str = knife_instance.tags_list(server.name).join(" ")
 | |
|             out << "Server tags: #{old_tags_str}\n"
 | |
|             knife_instance.tags_delete(server.name, old_tags_str)
 | |
| 
 | |
|             new_tags_str = tags.join(" ")
 | |
|             out << "Server new tags: #{new_tags_str}\n"
 | |
|             cmd = knife_instance.tags_create(server.name, new_tags_str)
 | |
|             unless cmd[1]
 | |
|               m = "Error: Cannot add tags '#{new_tags_str}' to server '#{server.name}'"
 | |
|               DevopsLogger.logger.error(m)
 | |
|               out.puts m
 | |
|               return 3
 | |
|             end
 | |
|             DevopsLogger.logger.info("New temporary tags for '#{server.name}': #{new_tags_str}")
 | |
|           end
 | |
| 
 | |
|           out << "\nBefore deploy hooks...\n"
 | |
|           res = self.run_hook(:before_deploy, out, deploy_info)
 | |
|           out << "Done\n"
 | |
|           out << "\nRun chef-client on '#{server.name}'\n"
 | |
|           cmd = server.remote_user == "root" ? "chef-client --no-color" : "sudo chef-client --no-color"
 | |
| 
 | |
|           ip = if server.public_ip.nil?
 | |
|             server.private_ip
 | |
|           else
 | |
|             out << "Public IP detected\n"
 | |
|             server.public_ip
 | |
|           end
 | |
| 
 | |
|           k = Devops::Model::Key.find(server.ssh_key)
 | |
|           json = {}
 | |
|           if deploy_info["use_json_file"]
 | |
|             deploy_info.delete("use_json_file")
 | |
|             dir = DevopsConfig.config[:project_info_dir]
 | |
|             file = deploy_info.delete("json_file") || "#{server.project}_#{server.deploy_env}_#{server.category}_#{Time.new.to_i}"
 | |
|             path = File.join(dir, file)
 | |
|             if File.exists?(path)
 | |
|               json = File.read(path)
 | |
|             else
 | |
|               json = JSON.pretty_generate(deploy_info)
 | |
|               File.open(File.join(dir, file), "w") do |f|
 | |
|                 f.write json
 | |
|               end
 | |
|             end
 | |
|           else
 | |
|             cmd << " -r #{deploy_info[:run_list].join(",")}" unless server.stack.nil?
 | |
|           end
 | |
|           out << "Deploy Input Parameters:\n"
 | |
|           out.puts json
 | |
|           out.flush
 | |
|           path = Devops::SSH::Utils.copy_deploy_info json, ip, server.remote_user, k.path, out
 | |
|           cmd << " -j #{path}"
 | |
|           out.flush
 | |
|           out << "Deploy command: #{cmd}\n"
 | |
|           status = Devops::SSH::Utils.run_command_out cmd, ip, server.remote_user, k.path, out
 | |
|           if status == 0
 | |
|             out << "\nAfter deploy hooks...\n"
 | |
|             res = self.run_hook(:after_deploy, out, deploy_info)
 | |
|             out << "Done\n"
 | |
|           else
 | |
|             raise Devops::Exception::DeployError.new("Chef client exit status code: '#{status}'")
 | |
|           end
 | |
|         ensure
 | |
|           unless old_tags_str.nil?
 | |
|             out << "Restore tags\n"
 | |
|             cmd = knife_instance.tags_delete(server.name, new_tags_str)
 | |
|             DevopsLogger.logger.info("Deleted tags for #{server.name}: #{new_tags_str}")
 | |
|             cmd = knife_instance.tags_create(server.name, old_tags_str)
 | |
|             DevopsLogger.logger.info("Set tags for #{server.name}: #{old_tags_str}")
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         private
 | |
|         def check_server_name server
 | |
|           knife_instance.chef_node_list.include?(server.cm_name) and knife_instance.chef_client_list.include?(server.cm_name)
 | |
|         end
 | |
| 
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 | 
