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