class MU::Master

Routines for use management and configuration on the Mu Master.

Constants

MY_HOME

Home directory of the invoking user

NAGIOS_HOME

Home directory of the Nagios user, if we're in a non-gem context

Public Class Methods

addHostToSSHConfig(server, ssh_dir: " click to toggle source

Insert a definition for a node into our SSH config. @param server [MU::Cloud::Server]: The name of the node. @param names [Array<String>]: Other names that we'd like this host to be known by for SSH purposes @param ssh_dir [String]: The configuration directory of the SSH config to emit. @param ssh_conf [String]: A specific SSH configuration file to write entries into. @param ssh_owner [String]: The preferred owner of the SSH configuration files. @param timeout [Integer]: An alternate timeout value for connections to this server. @return [void]

# File modules/mu/master.rb, line 584
    def self.addHostToSSHConfig(server,
        ssh_dir: "#{Etc.getpwuid(Process.uid).dir}/.ssh",
        ssh_conf: "#{Etc.getpwuid(Process.uid).dir}/.ssh/config",
        ssh_owner: Etc.getpwuid(Process.uid).name,
        names: [],
        timeout: 0
    )
      if server.nil?
        MU.log "Called addHostToSSHConfig without a MU::Cloud::Server object", MU::ERR, details: caller
        return nil
      end

      _nat_ssh_key, nat_ssh_user, nat_ssh_host, canonical_ip, ssh_user, ssh_key_name = begin
        server.getSSHConfig
      rescue MU::MuError
        return
      end

      if ssh_user.nil? or ssh_user.empty?
        MU.log "Failed to extract ssh_user for #{server.mu_name} addHostToSSHConfig", MU::ERR
        return
      end
      if canonical_ip.nil? or canonical_ip.empty?
        MU.log "Failed to extract canonical_ip for #{server.mu_name} addHostToSSHConfig", MU::ERR
        return
      end
      if ssh_key_name.nil? or ssh_key_name.empty?
        MU.log "Failed to extract ssh_key_name for #{server.mu_name} in addHostToSSHConfig", MU::ERR
        return
      end

      @ssh_semaphore.synchronize {

        if File.exist?(ssh_conf)
          File.readlines(ssh_conf).each { |line|
            if line.match(/^Host #{server.mu_name} /)
              MU.log("Attempt to add duplicate #{ssh_conf} entry for #{server.mu_name}", MU::WARN)
              return
            end
          }
        end

        File.open(ssh_conf, 'a', 0600) { |ssh_config|
          ssh_config.flock(File::LOCK_EX)
          host_str = "Host #{server.mu_name} #{server.canonicalIP}"
          if !names.nil? and names.size > 0
            host_str = host_str+" "+names.join(" ")
          end
          ssh_config.puts host_str
          ssh_config.puts "  Hostname #{server.canonicalIP}"
          if !nat_ssh_host.nil? and server.canonicalIP != nat_ssh_host
            ssh_config.puts "  ProxyCommand ssh -W %h:%p #{nat_ssh_user}@#{nat_ssh_host}"
          end
          if timeout > 0
            ssh_config.puts "  ConnectTimeout #{timeout}"
          end

          ssh_config.puts "  User #{ssh_user}"
# XXX I'd rather add the host key to known_hosts, but Net::SSH is a little dumb
          ssh_config.puts "  StrictHostKeyChecking no"
          ssh_config.puts "  ServerAliveInterval 60"

          ssh_config.puts "  IdentityFile #{ssh_dir}/#{ssh_key_name}"
          if !File.exist?("#{ssh_dir}/#{ssh_key_name}")
            MU.log "#{server.mu_name} - ssh private key #{ssh_dir}/#{ssh_key_name} does not exist", MU::WARN
          end

          ssh_config.flock(File::LOCK_UN)
          ssh_config.chown(Etc.getpwnam(ssh_owner).uid, Etc.getpwnam(ssh_owner).gid)
        }
        MU.log "Wrote #{server.mu_name} ssh key to #{ssh_dir}/config", MU::DEBUG
        return "#{ssh_dir}/#{ssh_key_name}"
      }
    end
addInstanceToEtcHosts(public_ip, chef_name = nil, system_name = nil) click to toggle source

Insert node names associated with a new instance into /etc/hosts so we can treat them as if they were real DNS entries. Especially helpful when Chef/Ohai mistake the proper hostname, e.g. when bootstrapping Windows. @param public_ip [String]: The node's IP address @param chef_name [String]: The node's Chef node name @param system_name [String]: The node's local system name @return [void]

# File modules/mu/master.rb, line 539
def self.addInstanceToEtcHosts(public_ip, chef_name = nil, system_name = nil)

  # XXX cover ipv6 case
  if public_ip.nil? or !public_ip.match(/^\d+\.\d+\.\d+\.\d+$/) or (chef_name.nil? and system_name.nil?)
    raise MuError, "addInstanceToEtcHosts requires public_ip and one or both of chef_name and system_name!"
  end
  if chef_name == "localhost" or system_name == "localhost"
    raise MuError, "Can't set localhost as a name in addInstanceToEtcHosts"
  end

  if !["mu", "root"].include?(MU.mu_user)
    response = nil
    begin
      response = open("https://127.0.0.1:#{MU.mommaCatPort.to_s}/rest/hosts_add/#{chef_name}/#{public_ip}").read
    rescue Errno::ECONNRESET, Errno::ECONNREFUSED
    end
    if response != "ok"
      MU.log "Unable to add #{public_ip} to /etc/hosts via MommaCat request", MU::WARN
    end
    return
  end

  File.readlines("/etc/hosts").each { |line|
    if line.match(/^#{public_ip} /) or (chef_name != nil and line.match(/ #{chef_name}(\s|$)/)) or (system_name != nil and line.match(/ #{system_name}(\s|$)/))
      MU.log "Ignoring attempt to add duplicate /etc/hosts entry: #{public_ip} #{chef_name} #{system_name}", MU::DEBUG
      return
    end
  }
  File.open("/etc/hosts", 'a') { |etc_hosts|
    etc_hosts.flock(File::LOCK_EX)
    etc_hosts.puts("#{public_ip} #{chef_name} #{system_name}")
    etc_hosts.flock(File::LOCK_UN)
  }
  MU.log("Added to /etc/hosts: #{public_ip} #{chef_name} #{system_name}")
end
applyKubernetesResources(name, blobs = [], kubeconfig: nil, outputdir: nil) click to toggle source

Given an array of hashes representing Kubernetes resources,

# File modules/mu/master.rb, line 417
def self.applyKubernetesResources(name, blobs = [], kubeconfig: nil, outputdir: nil)
  use_tmp = false
  if !outputdir
    require 'tempfile'
    use_tmp = true
  end

  count = 0
  blobs.each { |blob|
    f = nil
    blobfile = if use_tmp
      f = Tempfile.new("k8s-resource-#{count.to_s}-#{name}")
      f.puts blob.to_yaml
      f.close
      f.path
    else
      path = outputdir+"/k8s-resource-#{count.to_s}-#{name}"
      File.open(path, "w") { |fh|
        fh.puts blob.to_yaml
      }
      path
    end
    next if !kubectl
    done = false
    retries = 0
    begin
      %x{#{kubectl} --kubeconfig "#{kubeconfig}" get -f #{blobfile} > /dev/null 2>&1}
      arg = $?.exitstatus == 0 ? "apply" : "create"
      cmd = %Q{#{kubectl} --kubeconfig "#{kubeconfig}" #{arg} -f #{blobfile}}
      MU.log "Applying Kubernetes resource #{count.to_s} with kubectl #{arg}", MU::NOTICE, details: cmd
      output = %x{#{cmd} 2>&1}
      if $?.exitstatus == 0
        MU.log "Kubernetes resource #{count.to_s} #{arg} was successful: #{output}", details: blob.to_yaml
        done = true
      else
        MU.log "Kubernetes resource #{count.to_s} #{arg} failed: #{output}", MU::WARN, details: blob.to_yaml
        if retries < 5
          sleep 5
        else
          MU.log "Giving up on Kubernetes resource #{count.to_s} #{arg}"
          done = true
        end
        retries += 1
      end
      f.unlink if use_tmp
    end while !done
    count += 1
  }
end
cleanExpiredScratchpads() click to toggle source

Remove Scratchpad entries which have exceeded their maximum age.

# File modules/mu/master.rb, line 306
def self.cleanExpiredScratchpads
  return if !$MU_CFG['scratchpad'] or !$MU_CFG['scratchpad'].has_key?('max_age') or $MU_CFG['scratchpad']['max_age'] < 1
  @scratchpad_semaphore.synchronize {
    entries = MU::Groomer::Chef.getSecret(vault: "scratchpad")
    entries.each { |pad|
      data = MU::Groomer::Chef.getSecret(vault: "scratchpad", item: pad)
      if data["timestamp"].to_i < (Time.now.to_i - $MU_CFG['scratchpad']['max_age'])
        MU.log "Deleting expired Scratchpad entry #{pad}", MU::NOTICE
        MU::Groomer::Chef.deleteSecret(vault: "scratchpad", item: pad)
      end
    }
  }
end
deleteUser(user) click to toggle source

Remove a user from Chef, LDAP, and archive their home directory and metadata. @param user [String]

# File modules/mu/master.rb, line 141
def self.deleteUser(user)
  deletia = []
  begin
    home = Etc.getpwnam(user).dir
    if Dir.exist?(home)
      archive = "/home/#{user}.home.#{Time.now.to_i.to_s}.tar.gz"
      %x{/bin/tar -czpf #{archive} #{home}}
      MU.log "Archived #{user}'s home directory to #{archive}"
      deletia << home
    end
  end rescue ArgumentError
  if Dir.exist?("#{$MU_CFG['datadir']}/users/#{user}")
    archive = "#{$MU_CFG['datadir']}/#{user}.metadata.#{Time.now.to_i.to_s}.tar.gz"
    %x{/bin/tar -czpf #{archive} #{$MU_CFG['datadir']}/users/#{user}}
    MU.log "Archived #{user}'s Mu metadata cache to #{archive}"
    deletia << "#{$MU_CFG['datadir']}/users/#{user}"
  end
  MU::Master::Chef.deleteUser(user)
  MU::Master::LDAP.deleteUser(user)
  FileUtils.rm_rf(deletia)
end
disk(device, path, size = 50, cryptfile = nil, ramdisk = "ram7") click to toggle source

Create and mount a disk local to the Mu master, optionally using luks to encrypt it. This makes a few assumptions: that mu-master::init has been run, and that utilities like mkfs.xfs exist. TODO add parameters to use filesystems other than XFS, alternate paths, etc @param device [String]: The disk device, by the name we want to see from the OS side @param path [String]: The path where we'll mount the device @param size [Integer]: The size of the disk, in GB @param cryptfile [String]: The name of a luks encryption key, which we'll look for in MU.adminBucketName @param ramdisk [String]: The name of a ramdisk to use when mounting encrypted disks

# File modules/mu/master.rb, line 194
def self.disk(device, path, size = 50, cryptfile = nil, ramdisk = "ram7")
  temp_dev = "/dev/#{ramdisk}"

  if !File.open("/etc/mtab").read.match(/ #{path} /)
    realdevice = if MU::Cloud::Google.hosted?
      "/dev/disk/by-id/google-"+device.gsub(/.*?\/([^\/]+)$/, '\1')
    elsif MU::Cloud::AWS.hosted?
      MU::Cloud::AWS.realDevicePath(device.dup)
    else
      device.dup
    end
    alias_device = cryptfile ? "/dev/mapper/"+path.gsub(/[^0-9a-z_\-]/i, "_") : realdevice

    if !File.exist?(realdevice)
      MU.log "Creating #{path} volume"
      if MU::Cloud::AWS.hosted?
        dummy_svr = MU::Cloud::AWS::Server.new(
          mu_name: "MU-MASTER",
          cloud_id: MU.myInstanceId,
          kitten_cfg: {}
        )
        dummy_svr.addVolume(device, size)
        MU::Cloud::AWS::Server.tagVolumes(
          MU.myInstanceId,
          device: device,
          tag_name: "Name",
          tag_value: "#{$MU_CFG['hostname']} #{path}"
        )
        # the device might be on some arbitrary NVMe slot
        realdevice = MU::Cloud::AWS.realDevicePath(realdevice)
        alias_device = cryptfile ? "/dev/mapper/"+path.gsub(/[^0-9a-z_\-]/i, "_") : realdevice
      elsif MU::Cloud::Google.hosted?
        dummy_svr = MU::Cloud::Google::Server.new(
          mu_name: "MU-MASTER",
          cloud_id: MU.myInstanceId,
          kitten_cfg: { 'project' => MU::Cloud::Google.myProject, 'availability_zone' => MU.myAZ }
        )
        dummy_svr.addVolume(device, size) # This will tag itself sensibly
      else
        raise MuError, "Not in a familiar cloud, so I don't know how to create volumes for myself"
      end
    end

    if cryptfile
      body = nil
      if MU::Cloud::AWS.hosted?
        begin
          resp = MU::Cloud::AWS.s3.get_object(bucket: MU.adminBucketName, key: cryptfile)
          body = resp.body
        rescue StandardError => e
          MU.log "Failed to fetch #{cryptfile} from S3 bucket #{MU.adminBucketName}", MU::ERR, details: e.inspect
          %x{/bin/dd if=/dev/urandom of=#{temp_dev} bs=1M count=1 > /dev/null 2>&1}
          raise e
        end
      elsif MU::Cloud::Google.hosted?
        begin
          body = MU::Cloud::Google.storage.get_object(MU.adminBucketName, cryptfile)
        rescue StandardError => e
          MU.log "Failed to fetch #{cryptfile} from Cloud Storage bucket #{MU.adminBucketName}", MU::ERR, details: e.inspect
          %x{/bin/dd if=/dev/urandom of=#{temp_dev} bs=1M count=1 > /dev/null 2>&1}
          raise e
        end
      else
        raise MuError, "Not in a familiar cloud, so I don't know where to get my luks crypt key (#{cryptfile})"
      end

      keyfile = Tempfile.new(cryptfile)
      keyfile.puts body
      keyfile.close

      # we can assume that mu-master::init installed cryptsetup-luks
      if !File.exist?(alias_device)
        MU.log "Initializing crypto on #{alias_device}", MU::NOTICE
        %x{/sbin/cryptsetup luksFormat #{realdevice} #{keyfile.path} --batch-mode}
        %x{/sbin/cryptsetup luksOpen #{realdevice} #{alias_device.gsub(/.*?\/([^\/]+)$/, '\1')} --key-file #{keyfile.path}}
      end
      keyfile.unlink
    end

    %x{/usr/sbin/xfs_admin -l "#{alias_device}" > /dev/null 2>&1}
    if $?.exitstatus != 0
      MU.log "Formatting #{alias_device}", MU::NOTICE
      %x{/sbin/mkfs.xfs "#{alias_device}"}
      %x{/usr/sbin/xfs_admin -L "#{path.gsub(/[^0-9a-z_\-]/i, "_")}" "#{alias_device}"}
    end
    Dir.mkdir(path, 0700) if !Dir.exist?(path) # XXX recursive
    %x{/usr/sbin/xfs_info "#{alias_device}" > /dev/null 2>&1}
    if $?.exitstatus != 0
      MU.log "Mounting #{alias_device} to #{path}"
      %x{/bin/mount "#{alias_device}" "#{path}"}
    end

    if cryptfile
      %x{/bin/dd if=/dev/urandom of=#{temp_dev} bs=1M count=1 > /dev/null 2>&1}
    end

  end

end
diskUUID(dev) click to toggle source

Retrieve the UUID of a block device, if available @param dev [String]

# File modules/mu/master.rb, line 926
def self.diskUUID(dev)
  realdev = if MU::Cloud::Google.hosted?
    "/dev/disk/by-id/google-"+dev.gsub(/.*?\/([^\/]+)$/, '\1')
  elsif MU::Cloud::AWS.hosted?
    MU::Cloud::AWS.realDevicePath(dev)
  else
    dev
  end
  %x{/sbin/blkid #{realdev} -o export | grep ^UUID=}.chomp
end
fetchScratchPadSecret(itemname) click to toggle source

Retrieve a secret stored by storeScratchPadSecret, then delete it. @param itemname [String]: The identifier of the scratchpad secret.

# File modules/mu/master.rb, line 296
def self.fetchScratchPadSecret(itemname)
  @scratchpad_semaphore.synchronize {
    data = MU::Groomer::Chef.getSecret(vault: "scratchpad", item: itemname)
    raise MuError, "Malformed scratchpad secret #{itemname}" if !data.has_key?("secret")
    MU::Groomer::Chef.deleteSecret(vault: "scratchpad", item: itemname)
    return Base64.urlsafe_decode64(data["secret"])
  }
end
kubectl() click to toggle source

Locate a working kubectl executable and return its fully-qualified path.

# File modules/mu/master.rb, line 388
def self.kubectl
  return @@kubectl_path if @@kubectl_path

  paths = ["/opt/mu/bin"]+ENV['PATH'].split(/:/)
  best = nil
  best_version = nil
  paths.uniq.each { |path|
    path.sub!(/^~/, MY_HOME)
    if File.exist?(path+"/kubectl")
      version = %x{#{path}/kubectl version --short --client}.chomp.sub(/.*Client version:\s+v/i, '')
      next if !$?.success?
      if !best_version or MU.version_sort(best_version, version) > 0
        best_version = version
        best = path+"/kubectl"
      end
    end
  }
  if !best
    MU.log "Failed to find a working kubectl executable in any path", MU::WARN, details: paths.uniq.sort
    return nil
  else
    MU.log "Kubernetes commands will use #{best} (#{best_version})"
  end

  @@kubectl_path = best
  @@kubectl_path
end
listBlockDevices() click to toggle source

Just list our block devices @return [Array<String>]

# File modules/mu/master.rb, line 912
def self.listBlockDevices
  if File.executable?("/bin/lsblk")
    %x{/bin/lsblk -i -p -r -n | egrep ' disk( |$)'}.each_line.map { |l|
      l.chomp.sub(/ .*/, '')
    }
  else
    # XXX something dumber
    nil
  end
end
listUsers() click to toggle source

@return [Array<Hash>]: List of all Mu users, with pertinent metadata.

# File modules/mu/master.rb, line 321
def self.listUsers

  # Handle running in standalone/library mode, sans LDAP, gracefully
  if !$MU_CFG['multiuser']
    stub_user_data = {
      "mu" => {
        "email" => $MU_CFG['mu_admin_email'],
        "monitoring_email" => $MU_CFG['mu_admin_email'],
        "realname" => $MU_CFG['banner'],
        "admin" => true,
        "non_ldap" => true,
      }
    }
    if Etc.getpwuid(Process.uid).name != "root"
      stub_user_data[Etc.getpwuid(Process.uid).name] = stub_user_data["mu"].dup
    end

    return stub_user_data
  end

  if Etc.getpwuid(Process.uid).name != "root" or !Dir.exist?(MU.dataDir+"/users")
    username = Etc.getpwuid(Process.uid).name
    MU.log "Running without LDAP permissions to list users (#{username}), relying on Mu local cache", MU::DEBUG
    userdir = MU.mainDataDir+"/users/#{username}"
    all_user_data = {}
    all_user_data[username] = {}
    ["non_ldap", "email", "monitoring_email", "realname", "chef_user", "admin"].each { |field|
      if File.exist?(userdir+"/"+field)
        all_user_data[username][field] = File.read(userdir+"/"+field).chomp
      elsif ["email", "realname"].include?(field)
        MU.log "Required user field '#{field}' for '#{username}' not set in LDAP or in Mu's disk cache.", MU::WARN
      end
    }
    return all_user_data
  end
  # LDAP is canonical. Everything else is required to be in sync with it.
  ldap_users = MU::Master::LDAP.listUsers
  all_user_data = {}
  ldap_users['mu'] = {}
  ldap_users['mu']['admin'] = true
  ldap_users['mu']['non_ldap'] = true
  ldap_users.each_pair { |uname, data|
    key = uname.to_s
    all_user_data[key] = {}
    userdir = $MU_CFG['installdir']+"/var/users/#{key}"
    if !Dir.exist?(userdir)
      MU.log "No metadata exists for user #{key}, creating stub directory #{userdir}", MU::WARN
      Dir.mkdir(userdir, 0755)
    end

    ["non_ldap", "email", "monitoring_email", "realname", "chef_user", "admin"].each { |field|
      if data.has_key?(field)
        all_user_data[key][field] = data[field]
      elsif File.exist?(userdir+"/"+field)
        all_user_data[key][field] = File.read(userdir+"/"+field).chomp
      elsif ["email", "realname"].include?(field)
        MU.log "Required user field '#{field}' for '#{key}' not set in LDAP or in Mu's disk cache.", MU::WARN
      end
    }
  }
  all_user_data
end
manageUser( username, chef_username: nil, name: nil, email: nil, password: nil, admin: false, change_uid: -1, orgs: [], remove_orgs: [] ) click to toggle source

Create and/or update a user as appropriate (Chef, LDAP, et al). @param username [String]: The canonical username to modify. @param chef_username [String]: The Chef username, if different @param name [String]: Real name (Given Surname). Required for new accounts. @param email [String]: Email address of the user. Required for new accounts. @param password [String]: A password to set. Required for new accounts. @param admin [Boolean]: Whether or not the user should be a Mu admin. @param orgs [Array<String>]: Extra Chef organizations to which to add the user. @param remove_orgs [Array<String>]: Chef organizations from which to remove the user.

# File modules/mu/master.rb, line 84
def self.manageUser(
  username,
  chef_username: nil,
  name: nil,
  email: nil,
  password: nil,
  admin: false,
  change_uid: -1,
  orgs: [],
  remove_orgs: []
)
  create = false
  cur_users = listUsers
  create = true if !cur_users.has_key?(username)
  if !MU::Master::LDAP.manageUser(username, name: name, email: email, password: password, admin: admin, change_uid: change_uid)
    deleteUser(username) if create
    return false
  end
  %x{sh -x /etc/init.d/oddjobd start 2>&1 > /dev/null} # oddjobd dies, like a lot
  begin
    Etc.getpwnam(username)
  rescue ArgumentError
    return false
  end
  chef_username ||= username.dup
  %x{/bin/su - #{username} -c "ls > /dev/null"}
  if !MU::Master::Chef.manageUser(chef_username, ldap_user: username, name: name, email: email, admin: admin, orgs: orgs, remove_orgs: remove_orgs) and create
    deleteUser(username) if create
    return false
  end
  %x{/bin/su - #{username} -c "/opt/chef/bin/knife ssl fetch 2>&1 > /dev/null"}
  setLocalDataPerms(username)
  if create
    home = Etc.getpwnam(username).dir
    FileUtils.mkdir_p home+"/.mu/var"
    FileUtils.chown_R(username, username+".mu-user", Etc.getpwnam(username).dir)
    %x{/bin/su - #{username} -c "ls > /dev/null"}
    vars = {
      "home" => home,
      "installdir" => $MU_CFG['installdir']
    }
    File.open(home+"/.murc", "w+", 0640){ |f|
      f.puts Erubis::Eruby.new(File.read("#{$MU_CFG['libdir']}/install/user-dot-murc.erb")).result(vars)
    }
    File.open(home+"/.bashrc", "a"){ |f|
      f.puts "source #{home}/.murc"
    }
    FileUtils.chown_R(username, username+".mu-user", Etc.getpwnam(username).dir)
    %x{/sbin/restorecon -r /home}
  end
  true
end
nvme?() click to toggle source

Determine whether we're running in an NVMe-enabled environment

# File modules/mu/master.rb, line 938
def self.nvme?
  if File.executable?("/bin/lsblk")
    %x{/bin/lsblk -i -p -r -n}.each_line { |l|
      return true if l =~ /^\/dev\/nvme\d/
    }
  else
    return true if File.exists?("/dev/nvme0n1")
  end
  false
end
printUserDetails(user) click to toggle source

@param user [String]: The account name to display

# File modules/mu/master.rb, line 63
def self.printUserDetails(user)
  cur_users = listUsers

  if cur_users.has_key?(user)
    data = cur_users[user]
    puts "#{user.bold} - #{data['realname']} <#{data['email']}>"
    cur_users[user].each_pair { |key, val|
      puts "#{key}: #{val}"
    }
  end
end
printUsersToTerminal(users = MU::Master.listUsers) click to toggle source

@param users [Hash]: User metadata of the type returned by listUsers

# File modules/mu/master.rb, line 34
def self.printUsersToTerminal(users = MU::Master.listUsers)
  labeled = false
  users.keys.sort.each { |username|
    data = users[username]
    if data['admin']
      if !labeled
        labeled = true
        puts "Administrators".light_cyan.on_black.bold
      end
      append = ""
      append = " (Chef and local system ONLY)".bold if data['non_ldap']
      append = append + "(" + data['uid'] + ")" if data.has_key?('uid')
      puts "#{username.bold} - #{data['realname']} <#{data['email']}>"+append
    end
  }
  labeled = false
  users.keys.sort.each { |username|
    data = users[username]
    if !data['admin']
      if !labeled
        labeled = true
        puts "Regular users".light_cyan.on_black.bold
      end
      puts "#{username.bold} - #{data['realname']} <#{data['email']}>"
    end
  }
end
purgeDeployFromSSH(deploy_id, noop: false) click to toggle source

Evict ssh keys associated with a particular deploy from our ssh config and key directory. @param deploy_id [String] @param noop [Boolean]

# File modules/mu/master.rb, line 723
def self.purgeDeployFromSSH(deploy_id, noop: false)
  myhome = Etc.getpwuid(Process.uid).dir
  sshdir = "#{myhome}/.ssh"
  sshconf = "#{sshdir}/config"
  ssharchive = "#{sshdir}/archive"

  Dir.mkdir(sshdir, 0700) if !Dir.exist?(sshdir) and !noop
  Dir.mkdir(ssharchive, 0700) if !Dir.exist?(ssharchive) and !noop

  keyname = "deploy-#{deploy_id}"
  if File.exist?("#{sshdir}/#{keyname}")
    MU.log "Moving #{sshdir}/#{keyname} to #{ssharchive}/#{keyname}"
    if !noop
      File.rename("#{sshdir}/#{keyname}", "#{ssharchive}/#{keyname}")
    end
  end
  if File.exist?(sshconf) and File.open(sshconf).read.match(/\/deploy\-#{deploy_id}$/)
    MU.log "Expunging #{deploy_id} from #{sshconf}"
    if !noop
      FileUtils.copy(sshconf, "#{ssharchive}/config-#{deploy_id}")
      File.open(sshconf, File::CREAT|File::RDWR, 0600) { |f|
        f.flock(File::LOCK_EX)
        newlines = Array.new
        delete_block = false
        f.readlines.each { |line|
          if line.match(/^Host #{deploy_id}\-/)
            delete_block = true
          elsif line.match(/^Host /)
            delete_block = false
          end
          newlines << line if !delete_block
        }
        f.rewind
        f.truncate(0)
        f.puts(newlines)
        f.flush
        f.flock(File::LOCK_UN)
      }
    end
  end
  # XXX refactor with above? They're similar, ish.
  hostsfile = "/etc/hosts"
  if File.open(hostsfile).read.match(/ #{deploy_id}\-/)
    if Process.uid == 0
      MU.log "Expunging traces of #{deploy_id} from #{hostsfile}"
      if !noop
        FileUtils.copy(hostsfile, "#{hostsfile}.cleanup-#{deploy_id}")
        File.open(hostsfile, File::CREAT|File::RDWR, 0644) { |f|
          f.flock(File::LOCK_EX)
          newlines = Array.new
          f.readlines.each { |line|
            newlines << line if !line.match(/ #{deploy_id}\-/)
          }
          f.rewind
          f.truncate(0)
          f.puts(newlines)
          f.flush
          f.flock(File::LOCK_UN)
        }
      end
    else
      MU.log "Residual /etc/hosts entries for #{deploy_id} must be removed by root user", MU::WARN
    end
  end

end
removeHostFromSSHConfig(nodename, noop: false) click to toggle source

Clean a node's entries out of ~/.ssh/config @param nodename [String]: The node's name @return [void]

# File modules/mu/master.rb, line 690
def self.removeHostFromSSHConfig(nodename, noop: false)
  sshdir = "#{MY_HOME}/.ssh"
  sshconf = "#{sshdir}/config"

  if File.exist?(sshconf) and File.open(sshconf).read.match(/ #{nodename} /)
    MU.log "Expunging old #{nodename} entry from #{sshconf}", MU::DEBUG
    if !noop
      File.open(sshconf, File::CREAT|File::RDWR, 0600) { |f|
        f.flock(File::LOCK_EX)
        newlines = Array.new
        delete_block = false
        f.readlines.each { |line|
          if line.match(/^Host #{nodename}(\s|$)/)
            delete_block = true
          elsif line.match(/^Host /)
            delete_block = false
          end
          newlines << line if !delete_block
        }
        f.rewind
        f.truncate(0)
        f.puts(newlines)
        f.flush
        f.flock(File::LOCK_UN)
      }
    end
  end
end
removeIPFromSSHKnownHosts(ip, noop: false) click to toggle source

Clean an IP address out of ~/.ssh/known hosts @param ip [String]: The IP to remove @return [void]

# File modules/mu/master.rb, line 662
def self.removeIPFromSSHKnownHosts(ip, noop: false)
  return if ip.nil?
  sshdir = "#{MY_HOME}/.ssh"
  knownhosts = "#{sshdir}/known_hosts"

  if File.exist?(knownhosts) and File.open(knownhosts).read.match(/^#{Regexp.quote(ip)} /)
    MU.log "Expunging old #{ip} entry from #{knownhosts}", MU::NOTICE
    if !noop
      File.open(knownhosts, File::CREAT|File::RDWR, 0600) { |f|
        f.flock(File::LOCK_EX)
        newlines = Array.new
        f.readlines.each { |line|
          next if line.match(/^#{Regexp.quote(ip)} /)
          newlines << line
        }
        f.rewind
        f.truncate(0)
        f.puts(newlines)
        f.flush
        f.flock(File::LOCK_UN)
      }
    end
  end
end
removeInstanceFromEtcHosts(node) click to toggle source

Clean a node's entries out of /etc/hosts @param node [String]: The node's name @return [void]

# File modules/mu/master.rb, line 512
def self.removeInstanceFromEtcHosts(node)
  return if MU.mu_user != "mu"
  hostsfile = "/etc/hosts"
  FileUtils.copy(hostsfile, "#{hostsfile}.bak-#{MU.deploy_id}")
  File.open(hostsfile, File::CREAT|File::RDWR, 0644) { |f|
    f.flock(File::LOCK_EX)
    newlines = Array.new
    f.readlines.each { |line|
      newlines << line if !line.match(/ #{node}(\s|$)/)
    }
    f.rewind
    f.truncate(0)
    f.puts(newlines)
    f.flush

    f.flock(File::LOCK_UN)
  }
end
setLocalDataPerms(user) click to toggle source

Update Mu's local cache/metadata for the given user, fixing permissions and updating stored values. Create a single-user group for the user, as well. @param user [String]: The user to update @return [Integer]: The gid of the user's default group

# File modules/mu/master.rb, line 472
def self.setLocalDataPerms(user)
  userdir = $MU_CFG['datadir']+"/users/#{user}"
  retries = 0
  user = "root" if user == "mu"
  begin
    group = user == "root" ? Etc.getgrgid(0) : "#{user}.mu-user"
    if user != "root"
      MU.log "/usr/sbin/usermod -a -G '#{group}' '#{user}'", MU::DEBUG
      %x{/usr/sbin/usermod -a -G "#{group}" "#{user}"}
    end
    Dir.mkdir(userdir, 2750) if !Dir.exist?(userdir)
                            # XXX mkdir gets the perms wrong for some reason
    MU.log "/bin/chmod 2750 #{userdir}", MU::DEBUG
    %x{/bin/chmod 2750 #{userdir}}
    gid = user == "root" ? 0 : Etc.getgrnam(group).gid
    Dir.foreach(userdir) { |file|
      next if file == ".."
      File.chown(nil, gid, userdir+"/"+file)
      if File.file?(userdir+"/"+file)
        File.chmod(0640, userdir+"/"+file)
      end
    }
    return gid
  rescue ArgumentError => e
    if $MU_CFG["ldap"]["type"] == "Active Directory"
      puts %x{/usr/sbin/groupadd "#{user}.mu-user"}
    else
      MU.log "Got '#{e.message}' trying to set permissions on local files, will retry", MU::WARN
    end
    sleep 5
    if retries <= 5
      retries = retries + 1
      retry
    end
  end
end
storeScratchPadSecret(text) click to toggle source

Store a secret for end-user retrieval via MommaCat's public interface. @param text [String]:

# File modules/mu/master.rb, line 166
def self.storeScratchPadSecret(text)
  raise MuError, "Cannot store an empty secret in scratchpad" if text.nil? or text.empty?
  @scratchpad_semaphore.synchronize {
    itemname = nil
    data = {
      "secret" => Base64.urlsafe_encode64(text),
      "timestamp" => Time.now.to_i.to_s
    }
    begin
      itemname = Password.pronounceable(32)
      # Make sure this itemname isn't already in use
      MU::Groomer::Chef.getSecret(vault: "scratchpad", item: itemname)
    rescue MU::Groomer::MuNoSuchSecret
      MU::Groomer::Chef.saveSecret(vault: "scratchpad", item: itemname, data: data)
      return itemname
    end while true
  }
end
syncMonitoringConfig(blocking = true) click to toggle source

Ensure that the Nagios configuration local to the MU master has been updated, and make sure Nagios has all of the ssh keys it needs to tunnel to client nodes. @return [void]

# File modules/mu/master.rb, line 794
def self.syncMonitoringConfig(blocking = true)
  return if Etc.getpwuid(Process.uid).name != "root" or (MU.mu_user != "mu" and MU.mu_user != "root")
  parent_thread_id = Thread.current.object_id
  nagios_threads = []
  nagios_threads << Thread.new {
    MU.dupGlobals(parent_thread_id)
    realhome = Etc.getpwnam("nagios").dir
    [NAGIOS_HOME, "#{NAGIOS_HOME}/.ssh"].each { |dir|
      Dir.mkdir(dir, 0711) if !Dir.exist?(dir)
      File.chown(Etc.getpwnam("nagios").uid, Etc.getpwnam("nagios").gid, dir)
    }
    if realhome != NAGIOS_HOME and Dir.exist?(realhome) and !File.symlink?("#{realhome}/.ssh")
      File.rename("#{realhome}/.ssh", "#{realhome}/.ssh.#{$$}") if Dir.exist?("#{realhome}/.ssh")
      File.symlink("#{NAGIOS_HOME}/.ssh", Etc.getpwnam("nagios").dir+"/.ssh")
    end
    MU.log "Updating #{NAGIOS_HOME}/.ssh/config..."
    ssh_lock = File.new("#{NAGIOS_HOME}/.ssh/config.mu.lock", File::CREAT|File::TRUNC|File::RDWR, 0600)
    ssh_lock.flock(File::LOCK_EX)
    ssh_conf = File.new("#{NAGIOS_HOME}/.ssh/config.tmp", File::CREAT|File::TRUNC|File::RDWR, 0600)
    ssh_conf.puts "Host MU-MASTER localhost"
    ssh_conf.puts "  Hostname localhost"
    ssh_conf.puts "  User root"
    ssh_conf.puts "  IdentityFile #{NAGIOS_HOME}/.ssh/id_rsa"
    ssh_conf.puts "  StrictHostKeyChecking no"
    ssh_conf.close
    FileUtils.cp("#{Etc.getpwuid(Process.uid).dir}/.ssh/id_rsa", "#{NAGIOS_HOME}/.ssh/id_rsa")
    File.chown(Etc.getpwnam("nagios").uid, Etc.getpwnam("nagios").gid, "#{NAGIOS_HOME}/.ssh/id_rsa")
    threads = []

    parent_thread_id = Thread.current.object_id
    MU::MommaCat.listDeploys.sort.each { |deploy_id|
      begin
        # We don't want to use cached litter information here because this is also called by cleanTerminatedInstances.
        deploy = MU::MommaCat.getLitter(deploy_id)
        if deploy.ssh_key_name.nil? or deploy.ssh_key_name.empty?
          MU.log "Failed to extract ssh key name from #{deploy_id} in syncMonitoringConfig", MU::ERR if deploy.kittens.has_key?("servers")
          next
        end
        FileUtils.cp("#{Etc.getpwuid(Process.uid).dir}/.ssh/#{deploy.ssh_key_name}", "#{NAGIOS_HOME}/.ssh/#{deploy.ssh_key_name}")
        File.chown(Etc.getpwnam("nagios").uid, Etc.getpwnam("nagios").gid, "#{NAGIOS_HOME}/.ssh/#{deploy.ssh_key_name}")
        if deploy.kittens.has_key?("servers")
          deploy.kittens["servers"].values.each { |nodeclasses|
            nodeclasses.values.each { |nodes|
              nodes.values.each { |server|
                next if !server.cloud_desc
                MU.dupGlobals(parent_thread_id)
                threads << Thread.new {
                  MU::MommaCat.setThreadContext(deploy)
                  MU.log "Adding #{server.mu_name} to #{NAGIOS_HOME}/.ssh/config", MU::DEBUG
                  MU::Master.addHostToSSHConfig(
                      server,
                      ssh_dir: "#{NAGIOS_HOME}/.ssh",
                      ssh_conf: "#{NAGIOS_HOME}/.ssh/config.tmp",
                      ssh_owner: "nagios"
                  )
                  MU.purgeGlobals
                }
              }
            }
          }
        end
      rescue StandardError => e
        MU.log "#{e.inspect} while generating Nagios SSH config in #{deploy_id}", MU::ERR, details: e.backtrace
      end
    }
    threads.each { |t|
      t.join
    }
    ssh_lock.flock(File::LOCK_UN)
    ssh_lock.close
    File.chown(Etc.getpwnam("nagios").uid, Etc.getpwnam("nagios").gid, "#{NAGIOS_HOME}/.ssh/config.tmp")
    File.rename("#{NAGIOS_HOME}/.ssh/config.tmp", "#{NAGIOS_HOME}/.ssh/config")

    MU.log "Updating Nagios monitoring config, this may take a while..."
    output = nil
    if $MU_CFG and !$MU_CFG['master_runlist_extras'].nil?
      output = %x{#{MU::Groomer::Chef.chefclient} -o 'role[mu-master-nagios-only],#{$MU_CFG['master_runlist_extras'].join(",")}' 2>&1}
    else
      output = %x{#{MU::Groomer::Chef.chefclient} -o 'role[mu-master-nagios-only]' 2>&1}
    end

    if $?.exitstatus != 0
      MU.log "Nagios monitoring config update returned a non-zero exit code!", MU::ERR, details: output
    else
      MU.log "Nagios monitoring config update complete."
    end
  }

  if blocking
    nagios_threads.each { |t|
      t.join
    }
  end
end
zipDir(srcdir, outfile) click to toggle source

Recursively zip a directory @param srcdir [String] @param outfile [String]

# File modules/mu/master.rb, line 892
def self.zipDir(srcdir, outfile)
  require 'zip'
  ::Zip::File.open(outfile, ::Zip::File::CREATE) { |zipfile|
    addpath = Proc.new { |zip_path, parent_path|
      Dir.entries(parent_path).reject{ |d| [".", ".."].include?(d) }.each { |entry|
        src = File.join(parent_path, entry)
        dst = File.join(zip_path, entry).sub(/^\//, '')
        if File.directory?(src)
          addpath.call(dst, src)
        else
          zipfile.add(dst, src)
        end
      }
    }
    addpath.call("", srcdir)
  }
end