class MU::Master::Chef
Public Class Methods
Create and return a connection to the Chef
REST API. If we've already opened one, return that. @return [Chef::ServerAPI]
# File modules/mu/master/chef.rb, line 25 def self.chefAPI @chef_api ||= ::Chef::ServerAPI.new("https://#{$MU_CFG["public_address"]}:7443", client_name: "pivotal", signing_key_filename: "/etc/opscode/pivotal.pem") @chef_api end
Mangle Chef's server config to speak to LDAP
. Technically this only impacts logins for their web UI, which we currently don't use.
# File modules/mu/master/chef.rb, line 439 def self.configureChefForLDAP if $MU_CFG.has_key?("ldap") bind_creds = MU::Groomer::Chef.getSecret(vault: $MU_CFG["ldap"]["bind_creds"]["vault"], item: $MU_CFG["ldap"]["bind_creds"]["item"]) vars = { "server_url" => $MU_CFG["public_address"], "ldap" => true, "base_dn" => $MU_CFG["ldap"]["base_dn"], "group_dn" => $MU_CFG["ldap"]["admin_group_dn"], "dc" => $MU_CFG["ldap"]["dcs"].first, "bind_dn" => bind_creds[$MU_CFG["ldap"]["bind_creds"]["username_field"]], "bind_pw" => bind_creds[$MU_CFG["ldap"]["bind_creds"]["password_field"]], } chef_cfgfile = "/etc/opscode/chef-server.rb" chef_tmpfile = "#{chef_cfgfile}.tmp.#{Process.pid}" File.open(chef_tmpfile, File::CREAT|File::RDWR, 0644) { |f| f.puts Erubis::Eruby.new(File.read("#{$MU_CFG['libdir']}/install/chef-server.rb.erb")).result(vars) } new = File.read(chef_tmpfile) current = File.read(chef_cfgfile) if new != current MU.log "Updating #{chef_cfgfile}", MU::NOTICE File.rename(chef_tmpfile, chef_cfgfile) system("/opt/opscode/bin/chef-server-ctl reconfigure") else File.unlink(chef_tmpfile) end end end
@param user [String]: The regular, system name of the user @param chef_user [String]: The user's Chef
username, which may differ
# File modules/mu/master/chef.rb, line 105 def self.createUserClientCfg(user, chef_user) chefdir = Etc.getpwnam(user).dir+"/.chef" FileUtils.mkdir_p chefdir File.open(chefdir+"/client.rb.tmp.#{Process.pid}", File::CREAT|File::RDWR, 0640) { |f| f.puts "log_level :info" f.puts "log_location STDOUT" f.puts "chef_server_url 'https://#{$MU_CFG["public_address"]}/organizations/#{chef_user}'" f.puts "validation_client_name '#{chef_user}-validator'" } if !File.exist?("#{chefdir}/client.rb") or File.read("#{chefdir}/client.rb") != File.read("#{chefdir}/client.rb.tmp.#{Process.pid}") File.rename(chefdir+"/client.rb.tmp.#{Process.pid}", chefdir+"/client.rb") FileUtils.chown_R(user, user+".mu-user", Etc.getpwnam(user).dir+"/.chef") MU.log "Generated #{chefdir}/client.rb" else File.unlink("#{chefdir}/client.rb.tmp.#{Process.pid}") end end
@param user [String]: The regular, system name of the user @param chef_user [String]: The user's Chef
username, which may differ
# File modules/mu/master/chef.rb, line 126 def self.createUserKnifeCfg(user, chef_user) chefdir = Etc.getpwnam(user).dir+"/.chef" FileUtils.mkdir_p chefdir File.open(chefdir+"/knife.rb.tmp.#{Process.pid}", File::CREAT|File::RDWR, 0640) { |f| f.puts "log_level :info" f.puts "log_location STDOUT" f.puts "node_name '#{chef_user}'" f.puts "client_key '#{chefdir}/#{chef_user}.user.key'" f.puts "validation_client_name '#{chef_user}-validator'" f.puts "validation_key '#{chefdir}/#{chef_user}.org.key'" f.puts "chef_server_url 'https://#{$MU_CFG["public_address"]}:7443/organizations/#{chef_user}'" f.puts "chef_server_root 'https://#{$MU_CFG["public_address"]}:7443/organizations/#{chef_user}'" f.puts "syntax_check_cache_path '#{chefdir}/syntax_check_cache'" f.puts "cookbook_path [ '#{chefdir}/cookbooks', '#{chefdir}/site_cookbooks' ]" f.puts "knife[:vault_mode] = 'client'" f.puts "knife[:vault_admins] = ['#{chef_user}']" # f.puts "verify_api_cert false" # f.puts "ssl_verify_mode :verify_none" } if !File.exist?("#{chefdir}/knife.rb") or File.read("#{chefdir}/knife.rb") != File.read("#{chefdir}/knife.rb.tmp.#{Process.pid}") File.rename(chefdir+"/knife.rb.tmp.#{Process.pid}", chefdir+"/knife.rb") FileUtils.chown_R(user, user+".mu-user", Etc.getpwnam(user).dir+"/.chef") MU.log "Generated #{chefdir}/knife.rb" else File.unlink("#{chefdir}/knife.rb.tmp.#{Process.pid}") end end
Remove an organization from the Chef
server. @param org [String] @return [Boolean]
# File modules/mu/master/chef.rb, line 48 def self.deleteOrg(org) begin Timeout::timeout(45) { chefAPI.delete("organizations/#{org}") } MU.log "Removed Chef organization #{org}", MU::NOTICE return true rescue Timeout::Error MU.log "Timed out removing Chef organization #{org}, retrying", MU::WARN retry rescue Net::HTTPServerException => e if !e.message.match(/^404 /) MU.log "Couldn't remove Chef organization #{org}: #{e.message}", MU::WARN else MU.log "#{org} does not exist in Chef, cannot remove.", MU::DEBUG return false end return false end end
Remove a user account from the Chef
server. @param user [String] @return [Boolean]
# File modules/mu/master/chef.rb, line 72 def self.deleteUser(user) cur_users = MU::Master.listUsers chef_user = nil if cur_users.has_key?(user) and cur_users[user].has_key?("chef_user") chef_user = cur_users[user]["chef_user"] else chef_user = user end deleteOrg(chef_user) begin Timeout::timeout(45) { chefAPI.delete("users/#{chef_user}") } MU.log "Removed Chef user #{chef_user}", MU::NOTICE return true rescue Timeout::Error MU.log "Timed out removing Chef user #{chef_user}, retrying", MU::WARN retry rescue Net::HTTPServerException => e if !e.message.match(/^404 /) MU.log "Couldn't remove Chef user #{chef_user}: #{e.message}", MU::WARN else MU.log "#{chef_user} does not exist in Chef, cannot remove.", MU::DEBUG return false end return false end end
Fetch the Chef
server's metadata about an organization. Return nil if not found. @param org [String]: The name of the organization @return [Hash]
# File modules/mu/master/chef.rb, line 177 def self.getOrg(org) begin Timeout::timeout(45) { response = chefAPI.get("organizations/#{org}") return response } rescue Timeout::Error MU.log "Timed out fetching Chef organization #{org}, retrying", MU::WARN retry end rescue Net::HTTPServerException return nil end
@param user [String]: The user whose data we'll be fetching from the Chef
API. @return [<Hash>]
# File modules/mu/master/chef.rb, line 32 def self.getUser(user) begin Timeout::timeout(45) { response = chefAPI.get("users/#{user}") return response } rescue Timeout::Error MU.log "Timed out fetching Chef user #{user}, retrying", MU::WARN retry end rescue Net::HTTPServerException return nil end
Fetch the Chef
server's metadata about an organization. Return nil if not found. @param org [String]: The name of the organization @param fullname [String]: A more descriptive name for the organization. @param add_users [Array<String>]: Users to add to the org. @param remove_users [Array<String>]: Users to remove from the org. @return [Boolean]
# File modules/mu/master/chef.rb, line 196 def self.manageOrg(org, fullname: nil, add_users: [], remove_users: []) existing_org = getOrg(org) orgkey = nil add_users << "mu" if !add_users.include?("mu") and org != "mu" # This organization does not yet exist, create it if !existing_org begin org_data = { :name => org.dup, :full_name => fullname } Timeout::timeout(45) { response = chefAPI.post("organizations", org_data) MU.log "Created Chef organization #{org}", details: response orgkey = response["private_key"] add_users.each { |user| if getUser(user) == nil MU.log "Requested addition of Chef user #{user} to organization #{org}, but no such user exists", MU::WARN next end response = chefAPI.post("organizations/#{org}/association_requests", {:user => user}) association_id = response["uri"].split("/").last response = chefAPI.put("users/#{user}/association_requests/#{association_id}", { :response => 'accept' }) next if user == "mu" MU.log "Added user #{user} to Chef organization #{org}", details: response } } return orgkey rescue Net::HTTPServerException => e MU.log "Error setting up Chef organization #{org}: #{e.message}", MU::ERR, details: org_data return false rescue Timeout::Error MU.log "Timed out setting up Chef organization #{org}, retrying", MU::WARN retry end else begin Timeout::timeout(45) { add_users.each { |user| if getUser(user) == nil MU.log "Requested addition of Chef user #{user} to organization #{org}, but no such user exists", MU::WARN next end begin response = chefAPI.post("organizations/#{org}/association_requests", {:user => user}) rescue Net::HTTPServerException => e if e.message == '409 "Conflict"' next else raise e end end association_id = response["uri"].split("/").last response = chefAPI.put("users/#{user}/association_requests/#{association_id}", { :response => 'accept' }) next if user == "mu" MU.log "Added user #{user} to Chef organization #{org}", details: response } remove_users.each { |user| begin chefAPI.delete("organizations/#{org}/users/#{user}") MU.log "Removed Chef user #{user} from organization #{org}", MU::NOTICE rescue Net::HTTPServerException => e end } } rescue Timeout::Error MU.log "Timed out modifying Chef organization #{org}, retrying", MU::WARN retry end end return orgkey end
Call when creating or modifying a user. While Chef
technically does communicate with LDAP
, it's only for the web UI, which we don't even use. Keys still need to be managed, and sometimes the username can't even match the LDAP
one due to Chef's weird restrictions.
# File modules/mu/master/chef.rb, line 275 def self.manageUser(chef_user, name: nil, email: nil, orgs: [], remove_orgs: [], admin: false, ldap_user: nil, pass: nil) orgs = [] if orgs.nil? remove_orgs = [] if remove_orgs.nil? # In this shining future, there are no situations where we will *not* have # an LDAP user to link to. ldap_user = chef_user.dup if ldap_user.nil? if chef_user.gsub!(/\./, "") MU.log "Stripped . from username to create Chef user #{chef_user}.\nSee: https://github.com/chef/chef-server/issues/557", MU::NOTICE orgs.delete(ldap_user) end if admin orgs << "mu" else remove_orgs << "mu" end if remove_orgs.include?(chef_user) raise MU::MuError, "Can't remove Chef user #{chef_user} from the #{chef_user} org" end if (orgs & remove_orgs).size > 0 raise MU::MuError, "Cannot both add and remove from the same Chef org" end MU::Master.setLocalDataPerms(ldap_user) first = last = nil if !name.nil? last = name.split(/\s+/).pop first = name.split(/\s+/).shift end mangled_email = email.dup ext = getUser(chef_user) if !ext if name.nil? or email.nil? MU.log "Error creating Chef user #{chef_user}: Must supply real name and email address", MU::ERR return false end # We don't ever really need this password, so generate a random one if none # was supplied. if pass.nil? pass = (0...8).map { ('a'..'z').to_a[rand(26)] }.join end user_data = { :username => chef_user.dup, :first_name => first, :last_name => last, :display_name => name.dup, :email => email.dup, :create_key => true, :recovery_authentication_enabled => false, :external_authentication_uid => ldap_user.dup, :password => pass.dup } begin Timeout::timeout(45) { response = chefAPI.post("users", user_data) MU.log "Created Chef user #{chef_user}", details: response saveKey(ldap_user, "#{chef_user}.user.key", response["chef_key"]["private_key"]) key = manageOrg(chef_user, fullname: "#{name}'s Chef Organization", add_users: [chef_user]) if key saveKey(ldap_user, "#{chef_user}.org.key", key) end createUserKnifeCfg(ldap_user, chef_user) createUserClientCfg(ldap_user, chef_user) } rescue Timeout::Error MU.log "Timed out creating Chef user #{chef_user}, retrying", MU::WARN retry rescue Net::HTTPServerException => e # Work around Chef's baffling inability to use the same email address for # more than one user. # https://github.com/chef/chef-server/issues/59 if e.message.match(/409/) and !user_data[:email].match(/\+/) user_data[:email].sub!(/@/, "+"+(0...8).map { ('a'..'z').to_a[rand(26)] }.join+"@") retry end MU.log "Bad response when creating Chef user #{chef_user}: #{e.message}", MU::ERR, details: user_data return false end # This user exists, so modify it else retries = 0 begin user_data = { :username => chef_user, :recovery_authentication_enabled => false, :external_authentication_uid => ldap_user } ext.each_pair { |key, val| user_data[key.to_sym] = val } user_data[:display_name] = name.dup if !name.nil? user_data[:first_name] = first if !first.nil? user_data[:last_name] = last if !last.nil? user_data[:password] = pass.dup if !pass.nil? if !email.nil? if !user_data[:email].nil? mailbox, host = mangled_email.split(/@/) if !user_data[:email].match(/^#{Regexp.escape(mailbox)}\+.+?@#{Regexp.escape(host)}$/) user_data[:email] = mangled_email end else user_data[:email] = mangled_email end end Timeout::timeout(45) { response = chefAPI.put("users/#{chef_user}", user_data) user_data[:password] = "********" MU.log "Chef user #{chef_user} already exists, updating", details: user_data if response.has_key?("chef_key") and response["chef_key"].has_key?("private_key") saveKey(ldap_user, "#{chef_user}.user.key", response["chef_key"]["private_key"]) end } createUserKnifeCfg(ldap_user, chef_user) createUserClientCfg(ldap_user, chef_user) %{/bin/su "#{ldap_user}" -c "cd && /opt/chef/bin/knife ssl fetch"} rescue Timeout::Error MU.log "Timed out modifying Chef user #{chef_user}, retrying", MU::WARN retry rescue Net::HTTPServerException => e # Work around Chef's baffling inability to use the same email address for # more than one user. # https://github.com/chef/chef-server/issues/59 if e.message.match(/409/) and !user_data[:email].match(/\+/) if retries > 3 raise MU::MuError, "Got #{e.message} modifying Chef user #{chef_user} (#{user_data})" end sleep 5 retries = retries + 1 mangled_email.sub!(/@/, "+"+(0...8).map { ('a'..'z').to_a[rand(26)] }.join+"@") retry end MU.log "Failed to update user #{chef_user}: #{e.message}", MU::ERR, details: user_data raise e end end if ldap_user != chef_user File.open($MU_CFG['datadir']+"/users/#{ldap_user}/chef_user", File::CREAT|File::RDWR, 0640) { |f| f.puts chef_user } end orgs.each { |org| key = manageOrg(org, add_users: [chef_user]) if key saveKey(ldap_user, "#{org}.org.key", key) end } remove_orgs.each { |org| manageOrg(org, remove_users: [chef_user]) } # Meddling in the user's home directory # Make sure they'll trust the Chef server's SSL certificate MU::Master.setLocalDataPerms(ldap_user) true end
Save a Chef
key into both Mu's user metadata cache and the user's ~/.chef. @param user [String]: The (system) name of the user. @param keyname [String]: The name of the key, e.g. myuser.user.key or myuser.org.key @param key [String]: The Chef
private key to save
# File modules/mu/master/chef.rb, line 159 def self.saveKey(user, keyname, key) FileUtils.mkdir_p $MU_CFG['datadir']+"/users/#{user}" FileUtils.mkdir_p Etc.getpwnam(user).dir+"/.chef" [$MU_CFG['datadir']+"/users/#{user}/#{keyname}", Etc.getpwnam(user).dir+"/.chef/#{keyname}"].each { |keyfile| if File.exist?(keyfile) File.rename(keyfile, keyfile+"."+Time.now.to_i.to_s) end File.open(keyfile, File::CREAT|File::RDWR, 0640) { |f| f.puts key } MU.log "Wrote Chef key #{keyname} to #{keyfile}", MU::DEBUG } FileUtils.chown_R(user, user+".mu-user", Etc.getpwnam(user).dir+"/.chef") end