class MU::MommaCat

MommaCat is in charge of managing metadata about resources we've created, as well as orchestrating amongst them and bootstrapping nodes outside of the normal synchronous deploy sequence invoked by mu-deploy.

MommaCat is in charge of managing metadata about resources we've created, as well as orchestrating amongst them and bootstrapping nodes outside of the normal synchronous deploy sequence invoked by mu-deploy.

MommaCat is in charge of managing metadata about resources we've created, as well as orchestrating amongst them and bootstrapping nodes outside of the normal synchronous deploy sequence invoked by mu-deploy.

MommaCat is in charge of managing metadata about resources we've created, as well as orchestrating amongst them and bootstrapping nodes outside of the normal synchronous deploy sequence invoked by mu-deploy.

MommaCat is in charge of managing metadata about resources we've created, as well as orchestrating amongst them and bootstrapping nodes outside of the normal synchronous deploy sequence invoked by mu-deploy.

Constants

HABITAT_SYNONYMS

Lookup table to translate the word “habitat” back to its provider-specific jargon

Attributes

appname[R]
chef_user[R]
clouds[R]
deploy_id[R]
deploy_secret[R]
deployment[R]
environment[R]
handle[R]
initializing[R]
kittens[RW]
mu_user[R]
no_artifacts[R]
nocleanup[R]
original_config[R]
public_key[R]
seed[R]
ssh_key_name[R]
ssh_public_key[R]
timestamp[R]

Public Class Methods

cacheDeployMetadata(deploy_id = nil, use_cache: false) click to toggle source

Read all of our deployment.json files in and stick them in a hash. Used by search routines that just need to skim this data without loading entire {MU::MommaCat} objects.

# File modules/mu/mommacat/storage.rb, line 458
def self.cacheDeployMetadata(deploy_id = nil, use_cache: false)
  deploy_root = File.expand_path(MU.dataDir+"/deployments")
  MU::MommaCat.deploy_struct_semaphore.synchronize {
    @@deploy_cache ||= {}
    return if !Dir.exist?(deploy_root)

    Dir.entries(deploy_root).each { |deploy|
      this_deploy_dir = deploy_root+"/"+deploy
      this_deploy_file = this_deploy_dir+"/deployment.json"

      if deploy == "." or deploy == ".." or !Dir.exist?(this_deploy_dir) or
         (deploy_id and deploy_id != deploy) or
         !File.size?(this_deploy_file) or
         (use_cache and @@deploy_cache[deploy] and @@deploy_cache[deploy]['mtime'] == File.mtime(this_deploy_file))
        next
      end

      @@deploy_cache[deploy] ||= {}

      MU.log "Caching deploy #{deploy}", MU::DEBUG
      lock = File.open(this_deploy_file, File::RDONLY)
      lock.flock(File::LOCK_EX)
      @@deploy_cache[deploy]['mtime'] = File.mtime(this_deploy_file)

      begin
        @@deploy_cache[deploy]['data'] = JSON.parse(File.read(this_deploy_file))
        next if @@deploy_cache[deploy]['data'].nil?
        # Populate some generable entries that should be in the deploy
        # data. Also, bounce out if we realize we've found exactly what
        # we needed already.
        MU::Cloud.resource_types.values.each { |attrs|

          next if @@deploy_cache[deploy]['data'][attrs[:cfg_plural]].nil?
          if attrs[:has_multiples]
            @@deploy_cache[deploy]['data'][attrs[:cfg_plural]].each_pair { |node_class, nodes|
              next if nodes.nil? or !nodes.is_a?(Hash)
              nodes.each_pair { |nodename, data|
                next if !data.is_a?(Hash)
                data['#MU_NODE_CLASS'] ||= node_class
                data['#MU_NAME'] ||= nodename
                data["cloud"] ||= MU::Config.defaultCloud
              }
            }
          end
        }
      rescue JSON::ParserError
        raise MuError, "JSON parse failed on #{this_deploy_file}\n\n"+File.read(this_deploy_file)
      ensure
        lock.flock(File::LOCK_UN)
        lock.close
      end
    }
  }

  @@deploy_cache
end
cleanTerminatedInstances(debug = false) click to toggle source

Iterate over all known deployments and look for instances that have been terminated, but not yet cleaned up, then clean them up.

# File modules/mu/mommacat/daemon.rb, line 203
def self.cleanTerminatedInstances(debug = false)
  loglevel = debug ? MU::NOTICE : MU::DEBUG
  MU::MommaCat.lock("clean-terminated-instances", false, true)
  MU.log "Checking for harvested instances in need of cleanup", loglevel
  parent_thread_id = Thread.current.object_id
  purged = 0

  MU::MommaCat.listDeploys.each { |deploy_id|
    next if File.exist?(deploy_dir(deploy_id)+"/.cleanup")
    MU.log "Checking for dead wood in #{deploy_id}", loglevel
    need_reload = false
    @cleanup_threads << Thread.new {
      MU.dupGlobals(parent_thread_id)
      deploy = MU::MommaCat.getLitter(deploy_id, set_context_to_me: true)
      purged_this_deploy = 0
        MU.log "#{deploy_id} has some kittens in it", loglevel, details: deploy.kittens.keys
      if deploy.kittens.has_key?("servers")
        MU.log "#{deploy_id} has some servers declared", loglevel, details: deploy.object_id
        deploy.kittens["servers"].values.each { |nodeclasses|
          nodeclasses.each_pair { |nodeclass, servers|
            deletia = []
            MU.log "Checking status of servers under '#{nodeclass}'", loglevel, details: servers.keys
            servers.each_pair { |mu_name, server|
              server.describe
              if !server.cloud_id
                MU.log "Checking for presence of #{mu_name}, but unable to fetch its cloud_id", MU::WARN, details: server
              elsif !server.active?
                next if File.exist?(deploy_dir(deploy_id)+"/.cleanup-"+server.cloud_id)
                deletia << mu_name
                need_reload = true
                MU.log "Cleaning up metadata for #{server} (#{nodeclass}), formerly #{server.cloud_id}, which appears to have been terminated", MU::NOTICE
                begin
                  server.destroy
                  deploy.sendAdminMail("Retired metadata for terminated node #{mu_name}")
                  deploy.sendAdminSlack("Retired metadata for terminated node `#{mu_name}`")
                rescue StandardError => e
                  MU.log "Saw #{e.message} while retiring #{mu_name}", MU::ERR, details: e.backtrace
                  next
                end
                MU.log "Cleanup of metadata for #{server} (#{nodeclass}), formerly #{server.cloud_id} complete", MU::NOTICE
                purged = purged + 1
                purged_this_deploy = purged_this_deploy + 1
              end
            }
            deletia.each { |mu_name|
              servers.delete(mu_name)
            }
            if purged_this_deploy > 0
              # XXX triggering_node needs to take more than one node name
              deploy.syncLitter(servers.keys, triggering_node: deletia.first)
            end
          }
        }
      end
      if need_reload
        MU.log "Saving modified deploy #{deploy_id}", loglevel
        deploy.save!
        MU::MommaCat.getLitter(deploy_id)
      end
      MU.purgeGlobals
    }
  }
  @cleanup_threads.each { |t|
    t.join
  }
  MU.log "cleanTerminatedInstances threads complete", loglevel
  MU::MommaCat.unlock("clean-terminated-instances", true)
  @cleanup_threads = []

  if purged > 0
    if MU.myCloud == "AWS"
      MU::Cloud::AWS.openFirewallForClients # XXX add the other clouds, or abstract
    end
    MU::Master.syncMonitoringConfig
    GC.start
  end
  MU.log "cleanTerminatedInstances returning", loglevel
end
daemonLogFile() click to toggle source

Path to the log file used by the Momma Cat daemon @return [String]

# File modules/mu/mommacat/daemon.rb, line 284
def self.daemonLogFile
  base = (Process.uid == 0 and !MU.localOnly) ? "/var" : MU.dataDir
  "#{base}/log/mu-momma-cat.log"
end
daemonPidFile(root = false) click to toggle source

Path to the PID file used by the Momma Cat daemon @return [String]

# File modules/mu/mommacat/daemon.rb, line 291
def self.daemonPidFile(root = false)
  base = ((Process.uid == 0 or root) and !MU.localOnly) ? "/var" : MU.dataDir
  "#{base}/run/mommacat.pid"
end
deploy_dir(deploy_id) click to toggle source

Get the deploy directory @param deploy_id [String] @return [String]

# File modules/mu/mommacat/storage.rb, line 518
    def self.deploy_dir(deploy_id)
      raise MuError, "deploy_dir must get a deploy_id if called as class method (from #{caller[0]}; #{caller[1]})" if deploy_id.nil?
# XXX this will blow up if someone sticks MU in /
      path = File.expand_path(MU.dataDir+"/deployments")
      if !Dir.exist?(path)
        MU.log "Creating #{path}", MU::DEBUG
        Dir.mkdir(path, 0700)
      end
      path = path+"/"+deploy_id
      return path
    end
deploy_exists?(deploy_id) click to toggle source

Does the deploy with the given id exist? @param deploy_id [String] @return [String]

# File modules/mu/mommacat/storage.rb, line 533
def self.deploy_exists?(deploy_id)
  if deploy_id.nil? or deploy_id.empty?
    MU.log "Got nil deploy_id in MU::MommaCat.deploy_exists?", MU::WARN
    return
  end
  path = File.expand_path(MU.dataDir+"/deployments")
  if !Dir.exist?(path)
    Dir.mkdir(path, 0700)
  end
  deploy_path = File.expand_path(path+"/"+deploy_id)
  return Dir.exist?(deploy_path)
end
deploy_struct_semaphore() click to toggle source

Don't let things that modify the deploy struct Hash step on each other. @return [Mutex]

# File modules/mu/mommacat.rb, line 85
def self.deploy_struct_semaphore;
  @@deploy_struct_semaphore
end
findMatchingDeploy(origin) click to toggle source

Locate and return the deploy, if any, which matches the provided origin description @param origin [Hash]

# File modules/mu/mommacat/storage.rb, line 364
def self.findMatchingDeploy(origin)
  MU::MommaCat.listDeploys.each { |deploy_id|
    o_path = deploy_dir(deploy_id)+"/origin.json"
    next if !File.exist?(o_path)
    this_origin = JSON.parse(File.read(o_path))
    if origin == this_origin
      MU.log "Deploy #{deploy_id} matches origin hash, loading", details: origin
      return MU::MommaCat.new(deploy_id)
    end
  }
  nil
end
findStray(cloud, type, dummy_ok: false, no_deploy_search: false, allow_multi: false, deploy_id: nil, name: nil, mu_name: nil, cloud_id: nil, credentials: nil, region: nil, tag_key: nil, tag_value: nil, calling_deploy: MU.mommacat, habitats: [], **flags ) click to toggle source

Locate a resource that's either a member of another deployment, or of no deployment at all, and return a {MU::Cloud} object for it. @param cloud [String]: The Cloud provider to use. @param type [String]: The resource type. Can be the full class name, symbolic name, or Basket of Kittens configuration shorthand for the resource type. @param deploy_id [String]: The identifier of an outside deploy to search. @param name [String]: The name of the resource as defined in its 'name' Basket of Kittens field, typically used in conjunction with deploy_id. @param mu_name [String]: The fully-resolved and deployed name of the resource, typically used in conjunction with deploy_id. @param cloud_id [String]: A cloud provider identifier for this resource. @param region [String]: The cloud provider region @param tag_key [String]: A cloud provider tag to help identify the resource, used in conjunction with tag_value. @param tag_value [String]: A cloud provider tag to help identify the resource, used in conjunction with tag_key. @param allow_multi [Boolean]: Permit an array of matching resources to be returned (if applicable) instead of just one. @param dummy_ok [Boolean]: Permit return of a faked {MU::Cloud} object if we don't have enough information to identify a real live one. @return [Array<MU::Cloud>]

# File modules/mu/mommacat/search.rb, line 45
    def self.findStray(cloud, type,
        dummy_ok: false,
        no_deploy_search: false,
        allow_multi: false,
        deploy_id: nil,
        name: nil,
        mu_name: nil,
        cloud_id: nil,
        credentials: nil,
        region: nil,
        tag_key: nil,
        tag_value: nil,
        calling_deploy: MU.mommacat,
        habitats: [],
        **flags
      ) 
      _shortclass, _cfg_name, type, _classname, _attrs = MU::Cloud.getResourceNames(type, true)

      cloudclass = MU::Cloud.cloudClass(cloud)
      return nil if cloudclass.virtual?

      if (tag_key and !tag_value) or (!tag_key and tag_value)
        raise MuError, "Can't call findStray with only one of tag_key and tag_value set, must be both or neither"
      end

      credlist = credentials ? [credentials] : cloudclass.listCredentials

      # Help ourselves by making more refined parameters out of mu_name, if
      # they weren't passed explicitly
      if mu_name
        # We can extract a deploy_id from mu_name if we don't have one already
        deploy_id ||= mu_name.sub(/^(\w+-\w+-\d{10}-[A-Z]{2})-/, '\1')
        if !tag_key and !tag_value
          tag_key = "Name"
          tag_value = mu_name
        end
      end

      # See if the thing we're looking for is a member of the deploy that's
      # asking after it.
      if !deploy_id.nil? and !calling_deploy.nil? and
          calling_deploy.deploy_id == deploy_id and (!name.nil? or !mu_name.nil?)
        kitten = calling_deploy.findLitterMate(type: type, name: name, mu_name: mu_name, cloud_id: cloud_id, credentials: credentials)
        return [kitten] if !kitten.nil?
      end

      # See if we have it in deployment metadata generally
      kittens = {}
      if !no_deploy_search and (deploy_id or name or mu_name or cloud_id)
        kittens = search_my_deploys(type, deploy_id: deploy_id, name: name, mu_name: mu_name, cloud_id: cloud_id, credentials: credentials)
        return kittens.values if kittens.size == 1

        # We can't refine any further by asking the cloud provider...
        if kittens.size > 1 and !allow_multi and
           !cloud_id and !tag_key and !tag_value
          raise MultipleMatches, "Multiple matches in MU::MommaCat.findStray where none allowed from #{cloud}, #{type}, name: #{name}, mu_name: #{mu_name}, cloud_id: #{cloud_id}, credentials: #{credentials}, habitats: #{habitats} (#{caller(1..1)})"
        end
      end

      if !cloud_id and !(tag_key and tag_value) and (name or mu_name or deploy_id)
        return kittens.values
      end
      matches = []

      credlist.each { |creds|
        cur_habitats = []

        if habitats and !habitats.empty? and habitats != [nil]
          valid_habitats = cloudclass.listHabitats(creds)
          cur_habitats = (habitats & valid_habitats)
          next if cur_habitats.empty?
        else
          cur_habitats = cloudclass.listHabitats(creds)
        end

        cloud_descs = search_cloud_provider(type, cloud, cur_habitats, region, cloud_id: cloud_id, tag_key: tag_key, tag_value: tag_value, credentials: creds, flags: flags)

        cloud_descs.each_pair.each { |p, regions|
          regions.each_pair.each { |r, results|
            results.each_pair { |kitten_cloud_id, descriptor|
              # We already have a MU::Cloud object for this guy, use it
              if kittens.has_key?(kitten_cloud_id)
                matches << kittens[kitten_cloud_id]
              elsif dummy_ok and kittens.empty?
# XXX this is why this was threaded
                matches << generate_dummy_object(type, cloud, name, mu_name, kitten_cloud_id, descriptor, r, p, tag_value, calling_deploy, creds)
              end
            }
          }
        }
      }

      matches
    end
genUniquenessString() click to toggle source

Generate a three-character string which can be used to unique-ify the names of resources which might potentially collide, e.g. Windows local hostnames, Amazon Elastic Load Balancers, or server pool instances. @return [String]: A three-character string consisting of two alphnumeric characters (uppercase) and one number.

# File modules/mu/mommacat/naming.rb, line 133
def self.genUniquenessString
  begin
    candidate = SecureRandom.base64(2).slice(0..1) + SecureRandom.random_number(9).to_s
    candidate.upcase!
  end while candidate.match(/[^A-Z0-9]/)
  return candidate
end
generateHandle(seed) click to toggle source

Manufactures a human-readable deployment name from the random two-character seed in MU-ID. Cat-themed when possible. @param seed [String]: A two-character seed from which we'll generate a name. @return [String]: Two words

# File modules/mu/mommacat/naming.rb, line 374
def self.generateHandle(seed)
  word_one=word_two=nil

  # Unless we've got two letters that don't have corresponding cat-themed
  # words, we'll insist that our generated handle have at least one cat
  # element to it.
  require_cat_words = true
  if @catwords.select { |word| word.match(/^#{seed[0]}/i) }.size == 0 and
      @catwords.select { |word| word.match(/^#{seed[1]}/i) }.size == 0
    require_cat_words = false
    MU.log "Got an annoying pair of letters #{seed}, not forcing cat-theming", MU::DEBUG
  end
  allnouns = @catnouns + @jaegernouns
  alladjs = @catadjs + @jaegeradjs

  tries = 0
  begin
    # Try to avoid picking something "nouny" for the first word
    source = @catadjs + @catmixed + @jaegeradjs + @jaegermixed
    first_ltr = source.select { |word| word.match(/^#{seed[0]}/i) }
    if !first_ltr or first_ltr.size == 0
      first_ltr = @words.select { |word| word.match(/^#{seed[0]}/i) }
    end
    word_one = first_ltr.shuffle.first

    # If we got a paired set that happen to match our letters, go with it
    if !word_one.nil? and word_one.match(/-#{seed[1]}/i)
      word_one, word_two = word_one.split(/-/)
    else
      source = @words
      if @catwords.include?(word_one)
        source = @jaegerwords
      elsif require_cat_words
        source = @catwords
      end
      second_ltr = source.select { |word| word.match(/^#{seed[1]}/i) and !word.match(/-/i) }
      word_two = second_ltr.shuffle.first
    end
    tries = tries + 1
  end while tries < 50 and (word_one.nil? or word_two.nil? or word_one.match(/-/) or word_one == word_two or (allnouns.include?(word_one) and allnouns.include?(word_two)) or (alladjs.include?(word_one) and alladjs.include?(word_two)) or (require_cat_words and !@catwords.include?(word_one) and !@catwords.include?(word_two) and !@catwords.include?(word_one+"-"+word_two)))

  if tries >= 50 and (word_one.nil? or word_two.nil?)
    MU.log "I failed to generated a valid handle from #{seed}, faking it", MU::ERR
    return "#{seed[0].capitalize} #{seed[1].capitalize}"
  end

  return "#{word_one.capitalize} #{word_two.capitalize}"
end
getChunkName(obj, array_of = nil, habitat_translate: nil) click to toggle source

Given a piece of a BoK resource descriptor Hash, come up with shorthand strings to give it a name for human readers. If nothing reasonable can be extracted, returns nil. @param obj [Hash] @param array_of [String] @param habitat_translate [String] @return [Array<String,nil>]

# File modules/mu/mommacat/naming.rb, line 67
def self.getChunkName(obj, array_of = nil, habitat_translate: nil)
  return [nil, nil] if obj.nil?
  if [String, Integer, Boolean].include?(obj.class)
    return [obj, nil]
  end
  obj_type = array_of || obj['type']
  obj_name = obj['name'] || obj['id'] || obj['mu_name'] || obj['cloud_id']

  name_string = if obj_name
    if obj_type
      "#{obj_type}[#{obj_name}]"
    else
      obj_name.dup
    end
  else
    found_it = nil
    using = nil
    ["entity", "role"].each { |subtype|
      if obj[subtype] and obj[subtype].is_a?(Hash)
        found_it = if obj[subtype]["id"]
          obj[subtype]['id'].dup
        elsif obj[subtype]["type"] and obj[subtype]["name"]
          "#{obj[subtype]['type']}[#{obj[subtype]['name']}]"
        end
        break
      end
    }
    found_it
  end
  if name_string
    name_string.gsub!(/\[.+?\](\[.+?\]$)/, '\1')
    if habitat_translate and HABITAT_SYNONYMS[habitat_translate]
      name_string.sub!(/^habitats?\[(.+?)\]/i, HABITAT_SYNONYMS[habitat_translate]+'[\1]')
    end
  end

  location_list = []

  location = if obj['project']
    obj['project']
  elsif obj['habitat'] and (obj['habitat']['id'] or obj['habitat']['name'])
    obj['habitat']['name'] || obj['habitat']['id']
  else
    hab_str = nil
    ['projects', 'habitats'].each { |key|

      if obj[key] and obj[key].is_a?(Array)
        location_list = obj[key].sort.map { |p|
          (p["name"] || p["id"]).gsub(/^.*?[^\/]+\/([^\/]+)$/, '\1')
        }
        hab_str = location_list.join(", ")
        name_string.gsub!(/^.*?[^\/]+\/([^\/]+)$/, '\1') if name_string
        break
      end
    }
    hab_str
  end

  [name_string, location, location_list]
end
getLitter(deploy_id, set_context_to_me: false, use_cache: true) click to toggle source

Return a {MU::MommaCat} instance for an existing deploy. Use this instead of using initialize directly to avoid loading deploys multiple times or stepping on the global context for the deployment you're really working on.. @param deploy_id [String]: The deploy ID of the deploy to load. @param set_context_to_me [Boolean]: Whether new MommaCat objects should overwrite any existing per-thread global deploy variables. @param use_cache [Boolean]: If we have an existing object for this deploy, use that @return [MU::MommaCat]

# File modules/mu/mommacat/storage.rb, line 34
    def self.getLitter(deploy_id, set_context_to_me: false, use_cache: true)
      if deploy_id.nil? or deploy_id.empty?
        raise MuError, "Cannot fetch a deployment without a deploy_id"
      end

# XXX this caching may be harmful, causing stale resource objects to stick
# around. Have we fixed this? Sort of. Bad entries seem to have no kittens,
# so force a reload if we see that. That's probably not the root problem.
      littercache = nil
      begin
        @@litter_semaphore.synchronize {
          littercache = @@litters.dup
        }
        if littercache[deploy_id] and @@litters_loadtime[deploy_id]
          deploy_root = File.expand_path(MU.dataDir+"/deployments")
          this_deploy_dir = deploy_root+"/"+deploy_id
          if File.exist?("#{this_deploy_dir}/deployment.json")
            lastmod = File.mtime("#{this_deploy_dir}/deployment.json")
            if lastmod > @@litters_loadtime[deploy_id]
              MU.log "Deployment metadata for #{deploy_id} was modified on disk, reload", MU::NOTICE
              use_cache = false
            end
         end
        end
      rescue ThreadError => e
        # already locked by a parent caller and this is a read op, so this is ok
        raise e if !e.message.match(/recursive locking/)
        littercache = @@litters.dup
      end

      if !use_cache or littercache[deploy_id].nil?
        need_gc = !littercache[deploy_id].nil?
        newlitter = MU::MommaCat.new(deploy_id, set_context_to_me: set_context_to_me)
        # This, we have to synchronize, as it's a write
        @@litter_semaphore.synchronize {
          @@litters[deploy_id] = newlitter
          @@litters_loadtime[deploy_id] = Time.now
        }
        GC.start if need_gc
      elsif set_context_to_me
        MU::MommaCat.setThreadContext(@@litters[deploy_id])
      end
      return @@litters[deploy_id]
#     MU::MommaCat.new(deploy_id, set_context_to_me: set_context_to_me)
    end
guessName(desc, resourceclass, cloud_id: nil, tag_value: nil) click to toggle source

Given a cloud provider's native descriptor for a resource, make some reasonable guesses about what the thing's name should be.

# File modules/mu/mommacat/naming.rb, line 34
def self.guessName(desc, resourceclass, cloud_id: nil, tag_value: nil)
  if desc.respond_to?(:tags) and
     desc.tags.is_a?(Array) and
     desc.tags.first.respond_to?(:key) and
     desc.tags.map { |t| t.key }.include?("Name")
    desc.tags.select { |t| t.key == "Name" }.first.value
  else
    try = nil
    # Various GCP fields
    [:display_name, :name, (resourceclass.cfg_name+"_name").to_sym].each { |field|
      if desc.respond_to?(field) and desc.send(field).is_a?(String)
        try = desc.send(field)
        break
      end

    }
    try ||= if !tag_value.nil?
        tag_value
      else
        cloud_id
      end
    try
  end

end
listAllNodes() click to toggle source

Return a list of all nodes in all deployments. Does so without loading deployments fully. @return [Hash]

# File modules/mu/mommacat/storage.rb, line 324
def self.listAllNodes
  nodes = Hash.new
  MU::MommaCat.deploy_struct_semaphore.synchronize {
    MU::MommaCat.listDeploys.each { |deploy|
      if !Dir.exist?(MU::MommaCat.deploy_dir(deploy)) or
          !File.size?("#{MU::MommaCat.deploy_dir(deploy)}/deployment.json")
        MU.log "Didn't see deployment metadata for '#{deploy}'", MU::WARN
        next
      end
      data = File.open("#{MU::MommaCat.deploy_dir(deploy)}/deployment.json", File::RDONLY)
      MU.log "Getting lock to read #{MU::MommaCat.deploy_dir(deploy)}/deployment.json", MU::DEBUG
      data.flock(File::LOCK_EX)
      begin
        deployment = JSON.parse(File.read("#{MU::MommaCat.deploy_dir(deploy)}/deployment.json"))
        deployment["deploy_id"] = deploy
        if deployment.has_key?("servers")
          deployment["servers"].each_key { |nodeclass|
            deployment["servers"][nodeclass].each_pair { |mu_name, metadata|
              nodes[mu_name] = metadata
            }
          }
        end
      rescue JSON::ParserError => e
        MU.log "JSON parse failed on #{MU::MommaCat.deploy_dir(deploy)}/deployment.json", MU::ERR, details: e.message
      end
      data.flock(File::LOCK_UN)
      data.close
    }
  }
  return nodes
end
listDeploys() click to toggle source

Return a list of all currently active deploy identifiers. @return [Array<String>]

# File modules/mu/mommacat/storage.rb, line 311
def self.listDeploys
  return [] if !Dir.exist?("#{MU.dataDir}/deployments")
  deploys = []
  Dir.entries("#{MU.dataDir}/deployments").reverse_each { |muid|
    next if !Dir.exist?("#{MU.dataDir}/deployments/#{muid}") or muid == "." or muid == ".."
    deploys << muid
  }
  return deploys
end
listOptionalTags() click to toggle source

List the name/value pairs of our optional set of resource tags which should be applied to all taggable cloud provider resources. @return [Hash<String,String>]

# File modules/mu/mommacat/naming.rb, line 287
def self.listOptionalTags
  return {
    "MU-HANDLE" => MU.handle,
    "MU-MASTER-NAME" => Socket.gethostname,
    "MU-OWNER" => MU.mu_user
  }
end
listStandardTags() click to toggle source

List the name/value pairs for our mandatory standard set of resource tags, which should be applied to all taggable cloud provider resources. @return [Hash<String,String>]

# File modules/mu/mommacat/naming.rb, line 263
def self.listStandardTags
  return {} if !MU.deploy_id
  {
    "MU-ID" => MU.deploy_id,
    "MU-APP" => MU.appname,
    "MU-ENV" => MU.environment,
    "MU-MASTER-IP" => MU.mu_public_ip
  }
end
lock(id, nonblock = false, global = false, retries: 0, deploy_id: MU.deploy_id) click to toggle source

Create/hold a flock() lock. @param id [String]: The lock identifier to release. @param nonblock [Boolean]: Whether to block while waiting for the lock. In non-blocking mode, we simply return false if the lock is not available. return [false, nil]

# File modules/mu/mommacat/storage.rb, line 180
def self.lock(id, nonblock = false, global = false, retries: 0, deploy_id: MU.deploy_id)
  raise MuError, "Can't pass a nil id to MU::MommaCat.lock" if id.nil?

  if !global
    lockdir = "#{deploy_dir(deploy_id)}/locks"
  else
    lockdir = File.expand_path(MU.dataDir+"/locks")
  end

  if !Dir.exist?(lockdir)
    MU.log "Creating #{lockdir}", MU::DEBUG
    Dir.mkdir(lockdir, 0700)
  end
  nonblock = true if retries > 0

  @lock_semaphore.synchronize {
    if @locks[Thread.current.object_id].nil?
      @locks[Thread.current.object_id] = Hash.new
    end

    @locks[Thread.current.object_id][id] = File.open("#{lockdir}/#{id}.lock", File::CREAT|File::RDWR, 0600)
  }

  MU.log "Getting a lock on #{lockdir}/#{id}.lock (thread #{Thread.current.object_id})...", MU::DEBUG, details: caller
  show_relevant = Proc.new {
    @lock_semaphore.synchronize {
      @locks.each_pair { |thread_id, lock|
        lock.each_pair { |lockid, lockpath|
          if lockid == id
            thread = Thread.list.select { |t| t.object_id == thread_id }.first
            if thread.object_id != Thread.current.object_id
              MU.log "#{thread_id} sitting on #{id} (#{thread.thread_variables.map { |v| "#{v.to_s}: #{thread.thread_variable_get(v).to_s}" }.join(", ")})", MU::WARN, thread.backtrace
            end
          end
        }
      }
    }
  }

  begin
    if nonblock
      if !@locks[Thread.current.object_id][id].flock(File::LOCK_EX|File::LOCK_NB)
        if retries > 0
          success = false
          MU.retrier([], loop_if: Proc.new { !success }, loop_msg: "Waiting for lock on #{lockdir}/#{id}.lock...", max: retries, wait: 1, logmsg_interval: 0) { |cur_retries, _wait|
            success = @locks[Thread.current.object_id][id].flock(File::LOCK_EX|File::LOCK_NB)
            if !success and cur_retries > 0 and (cur_retries % 45) == 0
              show_relevant.call
            end
          }
          show_relevant.call if !success
          return success
        else
          return false
        end
      end
    else
      @locks[Thread.current.object_id][id].flock(File::LOCK_EX)
    end
  rescue IOError
    raise MU::BootstrapTempFail, "Interrupted waiting for lock on thread #{Thread.current.object_id}, probably just a node rebooting as part of a synchronous install"
  end
  MU.log "Lock on #{lockdir}/#{id}.lock on thread #{Thread.current.object_id} acquired", MU::DEBUG
  return true
end
locks() click to toggle source

List the currently held flock() locks.

# File modules/mu/mommacat/storage.rb, line 85
def self.locks;
  @lock_semaphore.synchronize {
    @locks
  }
end
nameKitten(server, sync_wait: false, no_dns: false) click to toggle source

Make sure the given node has proper DNS entries, /etc/hosts entries, SSH config entries, etc. @param server [MU::Cloud::Server]: The {MU::Cloud::Server} we'll be setting up. @param sync_wait [Boolean]: Whether to wait for DNS to fully synchronize before returning.

# File modules/mu/mommacat/naming.rb, line 299
    def self.nameKitten(server, sync_wait: false, no_dns: false)
      node, config, _deploydata = server.describe

      mu_zone = nil
      # XXX GCP!
      if !no_dns and MU::Cloud::AWS.hosted? and !MU::Cloud::AWS.isGovCloud?
        zones = MU::Cloud::DNSZone.find(cloud_id: "platform-mu")
        mu_zone = zones.values.first if !zones.nil?
      end

      if !mu_zone.nil?
        MU::Cloud::DNSZone.genericMuDNSEntry(name: node.gsub(/[^a-z0-9!"\#$%&'\(\)\*\+,\-\/:;<=>\?@\[\]\^_`{\|}~\.]/, '-').gsub(/--|^-/, ''), target: server.canonicalIP, cloudclass: MU::Cloud::Server, sync_wait: sync_wait)
      else
        MU::Master.addInstanceToEtcHosts(server.canonicalIP, node)
      end

## TO DO: Do DNS registration of "real" records as the last stage after the groomer completes
      if config && config['dns_records'] && !config['dns_records'].empty?
        dnscfg = config['dns_records'].dup
        dnscfg.each { |dnsrec|
          if !dnsrec.has_key?('name')
            dnsrec['name'] = node.downcase
            dnsrec['name'] = "#{dnsrec['name']}.#{MU.environment.downcase}" if dnsrec["append_environment_name"] && !dnsrec['name'].match(/\.#{MU.environment.downcase}$/)
          end

          if !dnsrec.has_key?("target")
            # Default to register public endpoint
            public = true

            if dnsrec.has_key?("target_type")
              # See if we have a preference for pubic/private endpoint
              public = dnsrec["target_type"] == "private" ? false : true
            end
  
            dnsrec["target"] =
              if dnsrec["type"] == "CNAME"
                if public
                  # Make sure we have a public canonical name to register. Use the private one if we don't
                  server.cloud_desc.public_dns_name.empty? ? server.cloud_desc.private_dns_name : server.cloud_desc.public_dns_name
                else
                  # If we specifically requested to register the private canonical name lets use that
                  server.cloud_desc.private_dns_name
                end
              elsif dnsrec["type"] == "A"
                if public
                  # Make sure we have a public IP address to register. Use the private one if we don't
                  server.cloud_desc.public_ip_address ? server.cloud_desc.public_ip_address : server.cloud_desc.private_ip_address
                else
                  # If we specifically requested to register the private IP lets use that
                  server.cloud_desc.private_ip_address
                end
              end
          end
        }
        if !MU::Cloud::AWS.isGovCloud?
          MU::Cloud::DNSZone.createRecordsFromConfig(dnscfg)
        end
      end

      MU::Master.removeHostFromSSHConfig(node)
      if server and server.canonicalIP
        MU::Master.removeIPFromSSHKnownHosts(server.canonicalIP)
      end
# XXX add names paramater with useful stuff
      MU::Master.addHostToSSHConfig(
          server,
          ssh_owner: server.deploy.mu_user,
          ssh_dir: Etc.getpwnam(server.deploy.mu_user).dir+"/.ssh"
      )
    end
name_unique_str_map() click to toggle source

Keep a map of the uniqueness strings we assign to various full names, in case we want to reuse them later. @return [Hash<String>]

# File modules/mu/mommacat/naming.rb, line 146
def self.name_unique_str_map
  @name_unique_str_map
end
new(deploy_id, create: false, deploy_secret: nil, config: nil, environment: "dev", ssh_key_name: nil, ssh_private_key: nil, ssh_public_key: nil, nocleanup: false, appname: nil, timestamp: nil, set_context_to_me: true, skip_resource_objects: false, no_artifacts: false, deployment_data: {}, delay_descriptor_load: false, mu_user: Etc.getpwuid(Process.uid).name ) click to toggle source

@param deploy_id [String]: The MU identifier of the deployment to load or create. @param create [Boolean]: Create a new deployment instead of searching for an existing one. @param deploy_secret [String]: A secret encrypted by the private key of a deployment we're loading. Used to validate remote requests to bootstrap into this deployment. @param config [Hash]: The full configuration, parsed by {MU::Config}, of this deployment. Required when creating a new deployment. @param environment [String]: The environment of a deployment to create. @param ssh_key_name [String]: Required when creating a new deployment. @param ssh_private_key [String]: Required when creating a new deployment. @param ssh_public_key [String]: SSH public key for authorized_hosts on clients. @param skip_resource_objects [Boolean]: Whether preload the cloud resource objects from this deploy. Can save load time for simple MommaCat tasks. @param nocleanup [Boolean]: Skip automatic cleanup of failed resources @param no_artifacts [Boolean]: Do not save deploy metadata @param deployment_data [Hash]: Known deployment data. @return [void]

# File modules/mu/mommacat.rb, line 126
    def initialize(deploy_id,
                   create: false,
                   deploy_secret: nil,
                   config: nil,
                   environment: "dev",
                   ssh_key_name: nil,
                   ssh_private_key: nil,
                   ssh_public_key: nil,
                   nocleanup: false,
                   appname: nil,
                   timestamp: nil,
                   set_context_to_me: true,
                   skip_resource_objects: false,
                   no_artifacts: false,
                   deployment_data: {},
                   delay_descriptor_load: false,
                   mu_user: Etc.getpwuid(Process.uid).name
    )
      if deploy_id.nil? or deploy_id.empty?
        raise DeployInitializeError, "MommaCat objects must specify a deploy_id"
      end
      set_context_to_me = true if create
      @initializing = true

      @deploy_id = deploy_id
      @mu_user = mu_user.dup
      @no_artifacts = no_artifacts

      # Make sure mu_user and chef_user are sane.
      if @mu_user == "root"
        @chef_user = "mu"
      else
        @chef_user = @mu_user.dup.delete(".")
        @mu_user = "root" if @mu_user == "mu"
      end
      @kitten_semaphore = Mutex.new
      @kittens = {}
      @original_config = MU::Config.manxify(config)
      @nocleanup = nocleanup
      @secret_semaphore = Mutex.new
      @notify_semaphore = Mutex.new
      @need_deploy_flush = false
      @node_cert_semaphore = Mutex.new
      @deployment = deployment_data

      @deployment['mu_public_ip'] = MU.mu_public_ip
      @private_key = nil
      @public_key = nil
      @secrets = Hash.new
      @secrets['instance_secret'] = Hash.new
      @secrets['windows_admin_password'] = Hash.new
      @ssh_key_name = ssh_key_name
      @ssh_private_key = ssh_private_key
      @ssh_public_key = ssh_public_key
      @clouds = {}
      @seed = MU.seed # pass this in
      @handle = MU.handle # pass this in
      @appname = appname
      @appname ||= @original_config['name'] if @original_config
      @timestamp = timestamp
      @environment = environment
      @original_config['environment'] ||= @environment if @original_config

      if set_context_to_me
        MU::MommaCat.setThreadContext(self)
      end

      if create and !@no_artifacts
        initDeployDirectory
        setDeploySecret
        MU::MommaCat.setThreadContext(self) if set_context_to_me
        save!
      end

      @appname ||= MU.appname
      @timestamp ||= MU.timestamp
      @environment ||= MU.environment

      loadDeploy(set_context_to_me: set_context_to_me)
      if !deploy_secret.nil? and !authKey(deploy_secret)
        raise DeployInitializeError, "Client request did not include a valid deploy authorization secret. Verify that userdata runs correctly?"
      end


      @@litter_semaphore.synchronize {
        @@litters[@deploy_id] ||= self
      }

      # Initialize a MU::Cloud object for each resource belonging to this
      # deploy, IF it already exists, which is to say if we're loading an
      # existing deploy instead of creating a new one.
      if !create and @deployment and @original_config and !skip_resource_objects
        loadObjects(delay_descriptor_load)
      end

      @initializing = false

# XXX this .owned? method may get changed by the Ruby maintainers
#     if !@@litter_semaphore.owned?
    end
purge(deploy_id) click to toggle source

Remove a deployment's metadata. @param deploy_id [String]: The deployment identifier to remove.

# File modules/mu/mommacat/storage.rb, line 276
def self.purge(deploy_id)
  if deploy_id.nil? or deploy_id.empty?
    raise MuError, "Got nil deploy_id in MU::MommaCat.purge"
  end
  # XXX archiving is better than annihilating
  path = File.expand_path(MU.dataDir+"/deployments")
  if Dir.exist?(path+"/"+deploy_id)
    unlockAll
    MU.log "Purging #{path}/#{deploy_id}" if File.exist?(path+"/"+deploy_id+"/deployment.json")

    FileUtils.rm_rf(path+"/"+deploy_id, :secure => true)
  end
  if File.exist?(path+"/unique_ids")
    File.open(path+"/unique_ids", File::CREAT|File::RDWR, 0600) { |f|
      newlines = []
      f.flock(File::LOCK_EX)
      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
end
restart() click to toggle source

(Re)start the Momma Cat daemon and return the exit status of the start command @return [Integer]

# File modules/mu/mommacat/daemon.rb, line 397
def self.restart
  stop
  start
end
setThreadContext(deploy) click to toggle source

Set the current threads' context (some knucklehead global variables) to values pertinent to the given deployment object. @param deploy [MU::MommaCat]: A deployment object

# File modules/mu/mommacat.rb, line 92
def self.setThreadContext(deploy)
  raise MuError, "Didn't get a MU::MommaCat object in setThreadContext" if !deploy.is_a?(MU::MommaCat)
  if !deploy.mu_user.nil?
    MU.setVar("chef_user", deploy.chef_user)
    if deploy.mu_user != "mu" and deploy.mu_user != "root"
      MU.setVar("dataDir", Etc.getpwnam(deploy.mu_user).dir+"/.mu/var")
      MU.setVar("mu_user", deploy.mu_user)
    else
      MU.setVar("dataDir", MU.mainDataDir)
      MU.setVar("mu_user", "root")
    end
  end
  MU.setVar("mommacat", deploy)
  MU.setVar("deploy_id", deploy.deploy_id)
  MU.setVar("appname", deploy.appname)
  MU.setVar("environment", deploy.environment)
  MU.setVar("timestamp", deploy.timestamp)
  MU.setVar("seed", deploy.seed)
  MU.setVar("handle", deploy.handle)
end
start() click to toggle source

Start the Momma Cat daemon and return the exit status of the command used @return [Integer]

# File modules/mu/mommacat/daemon.rb, line 298
def self.start
  if MU.inGem? and MU.muCfg['disable_mommacat']
    return
  end
  base = (Process.uid == 0 and !MU.localOnly) ? "/var" : MU.dataDir
  [base, "#{base}/log", "#{base}/run"].each { |dir|
   if !Dir.exist?(dir)
      MU.log "Creating #{dir}"
      Dir.mkdir(dir)
    end
  }
  if (Process.uid != 0 and
       (!$MU_CFG['overridden_keys'] or !$MU_CFG['overridden_keys'].include?("mommacat_port")) and
        status(true)
     ) or status
    return 0
  end

  File.unlink(daemonPidFile) if File.exists?(daemonPidFile)
  MU.log "Starting Momma Cat on port #{MU.mommaCatPort}, logging to #{daemonLogFile}, PID file #{daemonPidFile}"
  origdir = Dir.getwd
  Dir.chdir(MU.myRoot+"/modules")

  # XXX what's the safest way to find the 'bundle' executable in both gem and non-gem installs?
  if MU.inGem?
    cmd = %Q{thin --threaded --daemonize --port #{MU.mommaCatPort} --pid #{daemonPidFile} --log #{daemonLogFile} --ssl --ssl-key-file #{MU.muCfg['ssl']['key']} --ssl-cert-file #{MU.muCfg['ssl']['cert']} --ssl-disable-verify --tag mu-momma-cat -R mommacat.ru start}
  else
    cmd = %Q{bundle exec thin --threaded --daemonize --port #{MU.mommaCatPort} --pid #{daemonPidFile} --log #{daemonLogFile} --ssl --ssl-key-file #{MU.muCfg['ssl']['key']} --ssl-cert-file #{MU.muCfg['ssl']['cert']} --ssl-disable-verify --tag mu-momma-cat -R mommacat.ru start}
  end

  MU.log cmd, MU::NOTICE

  retries = 0
  begin
    output = %x{#{cmd}}
    sleep 1
    retries += 1
    if retries >= 10
      MU.log "MommaCat failed to start (command was #{cmd}, working directory #{MU.myRoot}/modules)", MU::WARN, details: output
      pp caller
      return $?.exitstatus
    end
  end while !status

  Dir.chdir(origdir)

  if $?.exitstatus != 0
    exit 1
  end

  return $?.exitstatus
end
status(root = false) click to toggle source

Return true if the Momma Cat daemon appears to be running @return [Boolean]

# File modules/mu/mommacat/daemon.rb, line 355
def self.status(root = false)
  if MU.inGem? and MU.muCfg['disable_mommacat']
    return true
  end
  if File.exist?(daemonPidFile(root))
    pid = File.read(daemonPidFile(root)).chomp.to_i
    begin
      Process.getpgid(pid)
      MU.log "Momma Cat running with pid #{pid.to_s}", (@@notified_on_pid[pid] ? MU::DEBUG : MU::INFO) # shush
      @@notified_on_pid[pid] = true
      return true
    rescue Errno::ESRCH
    end
  end
  MU.log "Momma Cat daemon not running", MU::NOTICE, details: daemonPidFile(root)
  false
end
stop() click to toggle source

Stop the Momma Cat daemon, if it's running

# File modules/mu/mommacat/daemon.rb, line 374
def self.stop
  if File.exist?(daemonPidFile)
    pid = File.read(daemonPidFile).chomp.to_i
    MU.log "Stopping Momma Cat with pid #{pid.to_s}"
    Process.kill("INT", pid)
    killed = false
    begin
      Process.getpgid(pid)
      sleep 1
    rescue Errno::ESRCH
      killed = true
    end while killed
    MU.log "Momma Cat with pid #{pid.to_s} stopped", MU::DEBUG, details: daemonPidFile

    begin
      File.unlink(daemonPidFile)
    rescue Errno::ENOENT
    end
  end
end
trapSafeLocks() click to toggle source

List the currently held flock() locks.

# File modules/mu/mommacat/storage.rb, line 81
def self.trapSafeLocks;
  @locks
end
unique_map_semaphore() click to toggle source

Keep a map of the uniqueness strings we assign to various full names, in case we want to reuse them later. @return [Mutex]

# File modules/mu/mommacat/naming.rb, line 153
def self.unique_map_semaphore
  @unique_map_semaphore
end
unlock(id, global = false, deploy_id: MU.deploy_id) click to toggle source

Release a flock() lock. @param id [String]: The lock identifier to release.

# File modules/mu/mommacat/storage.rb, line 248
def self.unlock(id, global = false, deploy_id: MU.deploy_id)
  raise MuError, "Can't pass a nil id to MU::MommaCat.unlock" if id.nil?
  lockdir = nil
  if !global
    lockdir = "#{deploy_dir(deploy_id)}/locks"
  else
    lockdir = File.expand_path(MU.dataDir+"/locks")
  end
  @lock_semaphore.synchronize {
    return if @locks.nil? or @locks[Thread.current.object_id].nil? or @locks[Thread.current.object_id][id].nil?
  }
  MU.log "Releasing lock on #{lockdir}/#{id}.lock (thread #{Thread.current.object_id})", MU::DEBUG
  begin
    @locks[Thread.current.object_id][id].flock(File::LOCK_UN)
    @locks[Thread.current.object_id][id].close
    if !@locks[Thread.current.object_id].nil?
      @locks[Thread.current.object_id].delete(id)
    end
    if @locks[Thread.current.object_id].size == 0
      @locks.delete(Thread.current.object_id)
    end
  rescue IOError => e
    MU.log "Got #{e.inspect} unlocking #{id} on #{Thread.current.object_id}", MU::WARN
  end
end
unlockAll() click to toggle source

Release all flock() locks held by the current thread.

# File modules/mu/mommacat/storage.rb, line 146
def self.unlockAll
  if !@locks.nil? and !@locks[Thread.current.object_id].nil?
    # Work from a copy so we can iterate without worrying about contention
    # in lock() or unlock(). We can't just wrap our iterator block in a
    # semaphore here, because we're calling another method that uses the
    # same semaphore.
    @lock_semaphore.synchronize {
      delete_list = []
      @locks[Thread.current.object_id].keys.each { |id|
        MU.log "Releasing lock on #{deploy_dir(MU.deploy_id)}/locks/#{id}.lock (thread #{Thread.current.object_id})", MU::DEBUG
        begin
          @locks[Thread.current.object_id][id].flock(File::LOCK_UN)
          @locks[Thread.current.object_id][id].close
        rescue IOError => e
          MU.log "Got #{e.inspect} unlocking #{id} on #{Thread.current.object_id}", MU::WARN
        end
        delete_list << id
      }
      # We do this here because we can't mangle a Hash while we're iterating
      # over it.
      delete_list.each { |id|
        @locks[Thread.current.object_id].delete(id)
      }
      if @locks[Thread.current.object_id].size == 0
        @locks.delete(Thread.current.object_id)
      end
    }
  end
end
updateLitter(deploy_id, litter) click to toggle source

Update the in-memory cache of a given deploy. This is intended for use by {#save!}, primarily. @param deploy_id [String] @param litter [MU::MommaCat]

# File modules/mu/mommacat.rb, line 53
def self.updateLitter(deploy_id, litter)
  return if litter.nil?
  @@litter_semaphore.synchronize {
    @@litters[deploy_id] = litter
    @@litters_loadtime[deploy_id] = Time.now
  }
end

Private Class Methods

generate_dummy_object(type, cloud, name, mu_name, cloud_id, desc, region, habitat, tag_value, calling_deploy, credentials) click to toggle source
# File modules/mu/mommacat/search.rb, line 250
def self.generate_dummy_object(type, cloud, name, mu_name, cloud_id, desc, region, habitat, tag_value, calling_deploy, credentials)
  resourceclass = MU::Cloud.resourceClass(cloud, type)

  use_name = if (name.nil? or name.empty?)
    if !mu_name.nil?
      mu_name
    else
      guessName(desc, resourceclass, cloud_id: cloud_id, tag_value: tag_value)
    end
  else
    name
  end

  if use_name.nil?
    return
  end

  cfg = {
    "name" => use_name,
    "cloud" => cloud,
    "credentials" => credentials
  }
  if !region.nil? and !resourceclass.isGlobal? 
    cfg["region"] = region
  end

  if resourceclass.canLiveIn.include?(:Habitat) and habitat
    cfg["project"] = habitat
  end

  # If we can at least find the config from the deploy this will
  # belong with, use that, even if it's an ungroomed resource.
  if !calling_deploy.nil? and
     !calling_deploy.original_config.nil? and
     !calling_deploy.original_config[type+"s"].nil?
    calling_deploy.original_config[type+"s"].each { |s|
      if s["name"] == use_name
        cfg = s.dup
        break
      end
    }

    return resourceclass.new(mommacat: calling_deploy, kitten_cfg: cfg, cloud_id: cloud_id)
  else
    if !@@dummy_cache[type] or !@@dummy_cache[type][cfg.to_s]
      newobj = resourceclass.new(mu_name: use_name, kitten_cfg: cfg, cloud_id: cloud_id, from_cloud_desc: desc)
      @@desc_semaphore.synchronize {
        @@dummy_cache[type] ||= {}
        @@dummy_cache[type][cfg.to_s] = newobj
      }
    end
    return @@dummy_cache[type][cfg.to_s]
  end
end
search_cloud_provider(type, cloud, habitats, region, cloud_id: nil, tag_key: nil, tag_value: nil, credentials: nil, flags: nil) click to toggle source
# File modules/mu/mommacat/search.rb, line 306
def self.search_cloud_provider(type, cloud, habitats, region, cloud_id: nil, tag_key: nil, tag_value: nil, credentials: nil, flags: nil)
  cloudclass = MU::Cloud.cloudClass(cloud)
  resourceclass = MU::Cloud.resourceClass(cloud, type)

  # Decide what regions we'll search, if applicable for this resource
  # type.
  regions = if resourceclass.isGlobal?
    [nil]
  else
    if region
      if region.is_a?(Array) and !region.empty?
        region
      else
        [region]
      end
    else
      cloudclass.listRegions(credentials: credentials)
    end
  end

  # Decide what habitats (accounts/projects/subscriptions) we'll
  # search, if applicable for this resource type.
  habitats ||= []
  if habitats.empty?
    if resourceclass.canLiveIn.include?(nil)
      habitats << nil
    end
    if resourceclass.canLiveIn.include?(:Habitat)
      habitats.concat(cloudclass.listHabitats(credentials, use_cache: false))
    end
  end
  habitats << nil if habitats.empty?
  habitats.uniq!

  cloud_descs = {}

  thread_waiter = Proc.new { |threads, threshold|
    begin
      threads.each { |t| t.join(0.1) }
      threads.reject! { |t| t.nil? or !t.alive? or !t.status }
      sleep 1 if threads.size > threshold
    end while threads.size > threshold
  }

  habitat_threads = []
  found_the_thing = false
  habitats.each { |hab|
    break if found_the_thing
    thread_waiter.call(habitat_threads, 5)

    habitat_threads << Thread.new(hab) { |habitat|
      cloud_descs[habitat] = {}
      region_threads = []
      regions.each { |reg|
        break if found_the_thing
        region_threads << Thread.new(reg) { |r|
          found = resourceclass.find(cloud_id: cloud_id, region: r, tag_key: tag_key, tag_value: tag_value, credentials: credentials, habitat: habitat, flags: flags)

          if found
            @@desc_semaphore.synchronize {
              cloud_descs[habitat][r] = found
            }
          end
          # Stop if you found the thing by a specific cloud_id
          if cloud_id and found and !found.empty?
            found_the_thing = true
          end
        }
      }
      thread_waiter.call(region_threads, 0)
    }
  }
  thread_waiter.call(habitat_threads, 0)

  cloud_descs
end
search_my_deploys(type, deploy_id: nil, name: nil, mu_name: nil, cloud_id: nil, credentials: nil) click to toggle source
# File modules/mu/mommacat/search.rb, line 384
    def self.search_my_deploys(type, deploy_id: nil, name: nil, mu_name: nil, cloud_id: nil, credentials: nil)
      kittens = {}
      _shortclass, _cfg_name, type, _classname, attrs = MU::Cloud.getResourceNames(type, true)

      # Check our in-memory cache of live deploys before resorting to
      # metadata
      littercache = nil
      # Sometimes we're called inside a locked thread, sometimes not. Deal
      # with locking gracefully.
      begin
        @@litter_semaphore.synchronize {
          littercache = @@litters.dup
        }
      rescue ThreadError => e
        raise e if !e.message.match(/recursive locking/)
        littercache = @@litters.dup
      end

      # First, see what we have in deploys that already happen to be loaded in
      # memory.
      littercache.each_pair { |cur_deploy, momma|
        next if deploy_id and deploy_id != cur_deploy

        @@deploy_struct_semaphore.synchronize {
          @deploy_cache[deploy_id] = {
            "mtime" => Time.now,
            "data" => momma.deployment
          }
        }

        straykitten = momma.findLitterMate(type: type, cloud_id: cloud_id, name: name, mu_name: mu_name, credentials: credentials, created_only: true)
        if straykitten
          MU.log "Found matching kitten #{straykitten.mu_name} in-memory - #{sprintf("%.2fs", (Time.now-start))}", MU::DEBUG
          # Peace out if we found the exact resource we want
          if cloud_id and straykitten.cloud_id.to_s == cloud_id.to_s
            return { straykitten.cloud_id => straykitten }
          elsif mu_name and straykitten.mu_name == mu_name
            return { straykitten.cloud_id => straykitten }
          else
            kittens[straykitten.cloud_id] ||= straykitten
          end
        end
      }

      # Now go rifle metadata from any other deploys we have on disk, if they
      # weren't already there in memory.
      cacheDeployMetadata(deploy_id) # freshen up @@deploy_cache
      mu_descs = {}
      if deploy_id.nil?
        @@deploy_cache.each_key { |deploy|
          next if littercache[deploy]
          next if !@@deploy_cache[deploy].has_key?('data')
          next if !@@deploy_cache[deploy]['data'].has_key?(type)
          if !name.nil?
            next if @@deploy_cache[deploy]['data'][type][name].nil?
            mu_descs[deploy] ||= []
            mu_descs[deploy] << @@deploy_cache[deploy]['data'][type][name].dup
          else
            mu_descs[deploy] ||= []
            mu_descs[deploy].concat(@@deploy_cache[deploy]['data'][type].values)
          end
        }
      elsif !@@deploy_cache[deploy_id].nil?
        if !@@deploy_cache[deploy_id]['data'].nil? and
            !@@deploy_cache[deploy_id]['data'][type].nil?
          if !name.nil? and !@@deploy_cache[deploy_id]['data'][type][name].nil?
            mu_descs[deploy_id] ||= []
            mu_descs[deploy_id] << @@deploy_cache[deploy_id]['data'][type][name].dup
          else
            mu_descs[deploy_id] = @@deploy_cache[deploy_id]['data'][type].values
          end
        end
      end

      mu_descs.each_pair { |deploy, matches|
        next if matches.nil? or matches.size == 0
        momma = MU::MommaCat.getLitter(deploy)

        # If we found exactly one match in this deploy, use its metadata to
        # guess at resource names we weren't told.
        straykitten = if matches.size > 1 and cloud_id
          momma.findLitterMate(type: type, cloud_id: cloud_id, credentials: credentials, created_only: true)
        elsif matches.size == 1 and (!attrs[:has_multiples] or matches.first.size == 1) and name.nil? and mu_name.nil?
          actual_data = attrs[:has_multiples] ? matches.first.values.first : matches.first
          if cloud_id.nil?
            momma.findLitterMate(type: type, name: (actual_data["name"] || actual_data["MU_NODE_CLASS"]), cloud_id: actual_data["cloud_id"], credentials: credentials)
          else
            momma.findLitterMate(type: type, name: (actual_data["name"] || actual_data["MU_NODE_CLASS"]), cloud_id: cloud_id, credentials: credentials)
          end
        else
          # There's more than one of this type of resource in the target
          # deploy, so see if findLitterMate can narrow it down for us
          momma.findLitterMate(type: type, name: name, mu_name: mu_name, cloud_id: cloud_id, credentials: credentials)
        end

        next if straykitten.nil?
        straykitten.intoDeploy(momma)

        if straykitten.cloud_id.nil?
          MU.log "findStray: kitten #{straykitten.mu_name} came back with nil cloud_id", MU::WARN
          next
        end
        next if cloud_id and straykitten.cloud_id.to_s != cloud_id.to_s

        # Peace out if we found the exact resource we want
        if (cloud_id and straykitten.cloud_id.to_s == cloud_id.to_s) or
           (mu_descs.size == 1 and matches.size == 1) or
           (credentials and straykitten.credentials == credentials)
# XXX strictly speaking this last check is only valid if findStray is searching
# exactly one set of credentials

          return { straykitten.cloud_id => straykitten }
        end

        kittens[straykitten.cloud_id] ||= straykitten
      }

      kittens
    end

Public Instance Methods

SSHKey() click to toggle source

Return the parts and pieces of this deploy's node ssh key set. Generate or load if that hasn't been done already.

# File modules/mu/mommacat.rb, line 492
    def SSHKey
      return [@ssh_key_name, @ssh_private_key, @ssh_public_key] if !@ssh_key_name.nil?
      if numKittens(types: ["Server", "ServerPool", "ContainerCluster"]) == 0
        return []
      end
      @ssh_key_name="deploy-#{MU.deploy_id}"
      ssh_dir = Etc.getpwnam(@mu_user).dir+"/.ssh"

      if !File.directory?(ssh_dir) then
        MU.log "Creating #{ssh_dir}", MU::DEBUG
        Dir.mkdir(ssh_dir, 0700)
        if Process.uid == 0 and @mu_user != "mu"
          FileUtils.chown Etc.getpwnam(@mu_user).uid, Etc.getpwnam(@mu_user).gid, ssh_dir
        end
      end
      if !File.exist?("#{ssh_dir}/#{@ssh_key_name}")
        MU.log "Generating SSH key #{@ssh_key_name}"
        %x{/usr/bin/ssh-keygen -N "" -f #{ssh_dir}/#{@ssh_key_name}}
      end
      @ssh_public_key = File.read("#{ssh_dir}/#{@ssh_key_name}.pub")
      @ssh_public_key.chomp!
      @ssh_private_key = File.read("#{ssh_dir}/#{@ssh_key_name}")
      @ssh_private_key.chomp!

# XXX the following mess belongs in cloud layers, probably in their initDeploy
# methods
      if numKittens(clouds: ["AWS"], types: ["Server", "ServerPool", "ContainerCluster"]) > 0
        creds_used = []
        ["servers", "server_pools", "container_clusters"].each { |type|
          next if @original_config[type].nil?
          @original_config[type].each { |descriptor|
            next if descriptor['cloud'] != "AWS"
            if descriptor['credentials']
              creds_used << descriptor['credentials']
            else
              creds_used << MU::Cloud::AWS.credConfig(name_only: true)
            end
          }
        }
        creds_used << nil if creds_used.empty?

        creds_used.uniq.each { |credset|
          MU::Cloud::AWS.createEc2SSHKey(@ssh_key_name, @ssh_public_key, credentials: credset)
        }
      end

      return [@ssh_key_name, @ssh_private_key, @ssh_public_key]
    end
addKitten(type, name, object, do_notify: false) click to toggle source

Keep tabs on a {MU::Cloud} object so that it can be found easily by findLitterMate. @param type [String]: @param name [String]: @param object [MU::Cloud]:

# File modules/mu/mommacat.rb, line 403
def addKitten(type, name, object, do_notify: false)
  if !type or !name or !object or !object.mu_name
    raise MuError, "Nil arguments to addKitten are not allowed (got type: #{type}, name: #{name}, and '#{object}' to add)"
  end

  _shortclass, _cfg_name, type, _classname, attrs = MU::Cloud.getResourceNames(type)
  object.intoDeploy(self)

  add_block = Proc.new {
    @kittens[type] ||= {}
    @kittens[type][object.habitat] ||= {}
    if attrs[:has_multiples]
      @kittens[type][object.habitat][name] ||= {}
      @kittens[type][object.habitat][name][object.mu_name] = object
    else
      @kittens[type][object.habitat][name] = object
    end
    if do_notify
      notify(type, name, object.notify, triggering_node: object, delayed_save: true)
    end
  }

  begin
    @kitten_semaphore.synchronize {
      add_block.call()
    }
  rescue ThreadError => e
    # already locked by a parent call to this method, so this should be safe
    raise e if !e.message.match(/recursive locking/)
    add_block.call()
  end
end
authKey(ciphertext) click to toggle source

Check a provided deploy key against our stored version. The instance has in theory accessed a secret via S3 and encrypted it with the deploy's public key. If it decrypts correctly, we assume this instance is indeed one of ours. @param ciphertext [String]: The text to decrypt. return [Boolean]: Whether the provided text was encrypted with the correct key

# File modules/mu/mommacat/daemon.rb, line 28
def authKey(ciphertext)
  if @private_key.nil? or @deploy_secret.nil?
    MU.log "Missing auth metadata, can't authorize node in authKey", MU::ERR
    return false
  end
  my_key = OpenSSL::PKey::RSA.new(@private_key)

  begin
    if my_key.private_decrypt(ciphertext).force_encoding("UTF-8").chomp == @deploy_secret.force_encoding("UTF-8").chomp
      MU.log "Matched ciphertext for #{MU.deploy_id}", MU::INFO
      return true
    else
      MU.log "Mis-matched ciphertext for #{MU.deploy_id}", MU::ERR
      return false
    end
  rescue OpenSSL::PKey::RSAError => e
    MU.log "Error decrypting provided ciphertext using private key from #{deploy_dir}/private_key: #{e.message}", MU::ERR, details: ciphertext
    return false
  end
end
cloudsUsed() click to toggle source

List all the cloud providers declared by resources in our deploy.

# File modules/mu/mommacat.rb, line 228
def cloudsUsed
  seen = []
  seen << @original_config['cloud'] if @original_config['cloud']
  MU::Cloud.resource_types.each_value { |attrs|
    type = attrs[:cfg_plural]
    if @original_config[type]
      @original_config[type].each { |resource|
        seen << resource['cloud'] if resource['cloud']
      }
    end
  }
  seen.uniq
end
credsUsed() click to toggle source

Assay this deployment for a list of credentials (from mu.yaml) which are used. Our Cleanup module can leverage this to skip unnecessary checks. @return [Array<String>]

# File modules/mu/mommacat.rb, line 245
    def credsUsed
      return [] if !@original_config
      seen = []
#      clouds = []
      seen << @original_config['credentials'] if @original_config['credentials']
#      defaultcloud = @original_config['cloud']
      MU::Cloud.resource_types.each_value { |attrs|
        type = attrs[:cfg_plural]
        if @original_config[type]
          @original_config[type].each { |resource|
            if resource['credentials']
              seen << resource['credentials']
            else
              cloudconst = @original_config['cloud'] ? @original_config['cloud'] : MU::Config.defaultCloud
              seen << MU::Cloud.cloudClass(cloudconst).credConfig(name_only: true)
            end
          }
        end
      }
# XXX insert default for each cloud provider if not explicitly seen
      seen.uniq
    end
decryptWithDeployKey(ciphertext) click to toggle source

Decrypt a string with the deployment's private key. @param ciphertext [String]: The string to decrypt

# File modules/mu/mommacat.rb, line 445
def decryptWithDeployKey(ciphertext)
  my_private_key = OpenSSL::PKey::RSA.new(@private_key)
  return my_private_key.private_decrypt(ciphertext)
end
deploy_dir() click to toggle source

@return [String]: The Mu Master filesystem directory holding metadata for the current deployment

# File modules/mu/mommacat/storage.rb, line 357
def deploy_dir
  MU::MommaCat.deploy_dir(@deploy_id)
end
encryptWithDeployKey(ciphertext) click to toggle source

Encrypt a string with the deployment's public key. @param ciphertext [String]: The string to encrypt

# File modules/mu/mommacat.rb, line 438
def encryptWithDeployKey(ciphertext)
  my_public_key = OpenSSL::PKey::RSA.new(@public_key)
  return my_public_key.public_encrypt(ciphertext)
end
fetchSecret(instance_id, type, quiet: false) click to toggle source

Retrieve an encrypted secret from metadata for the current deployment. @param instance_id [String]: The cloud instance identifier with which this secret is associated. @param type [String]: The type of secret, used to identify for retrieval. @param quiet [Boolean]: Do not log errors for non-existent secrets

# File modules/mu/mommacat.rb, line 476
def fetchSecret(instance_id, type, quiet: false)
  @secret_semaphore.synchronize {
    if @secrets[type].nil?
      return nil if quiet
      raise SecretError, "'#{type}' is not a valid secret type (valid types: #{@secrets.keys.join(", ")})"
    end
    if @secrets[type][instance_id].nil?
      return nil if quiet
      raise SecretError, "No '#{type}' secret known for instance #{instance_id}"
    end
  }
  return decryptWithDeployKey(@secrets[type][instance_id])
end
findLitterMate(type: nil, name: nil, mu_name: nil, cloud_id: nil, created_only: false, return_all: false, credentials: nil, habitat: nil, ignore_missing: false, debug: false, **flags) click to toggle source

Return the resource object of another member of this deployment @param type [String,Symbol]: The type of resource @param name [String]: The name of the resource as defined in its 'name' Basket of Kittens field @param mu_name [String]: The fully-resolved and deployed name of the resource @param cloud_id [String]: The cloud provider's unique identifier for this resource @param created_only [Boolean]: Only return the littermate if its cloud_id method returns a value @param return_all [Boolean]: Return a Hash of matching objects indexed by their mu_name, instead of a single match. Only valid for resource types where has_multiples is true. @return [MU::Cloud]

# File modules/mu/mommacat/search.rb, line 150
def findLitterMate(type: nil, name: nil, mu_name: nil, cloud_id: nil, created_only: false, return_all: false, credentials: nil, habitat: nil, ignore_missing: false, debug: false, **flags)
  _shortclass, _cfg_name, type, _classname, attrs = MU::Cloud.getResourceNames(type)

  # If we specified a habitat, which we may also have done by its shorthand
  # sibling name, or a Ref. Convert to something we can use.
  habitat = resolve_habitat(habitat, credentials: credentials)

  nofilter = (mu_name.nil? and cloud_id.nil? and credentials.nil?)

  does_match = Proc.new { |obj|

    (!created_only or !obj.cloud_id.nil?) and (nofilter or (
      (mu_name and obj.mu_name and mu_name.to_s == obj.mu_name) or
      (cloud_id and obj.cloud_id and cloud_id.to_s == obj.cloud_id.to_s) or
      (credentials and obj.credentials and credentials.to_s == obj.credentials.to_s) and
      !(
        (mu_name and obj.mu_name and mu_name.to_s != obj.mu_name) or
        (cloud_id and obj.cloud_id and cloud_id.to_s != obj.cloud_id.to_s) or
        (credentials and obj.credentials and credentials.to_s != obj.credentials.to_s)
      )
    ))
  }

  @kitten_semaphore.synchronize {

    if !@kittens.has_key?(type)
      return nil if !@original_config or @original_config[type].nil? or @original_config[type].empty?
      begin
        loadObjects(false)
      rescue ThreadError => e
        if e.message !~ /deadlock/
          raise e
        end
      end
      if @object_load_fails or !@kittens[type]
        if !ignore_missing
          MU.log "#{@deploy_id}'s original config has #{@original_config[type].size == 1 ? "a" : @original_config[type].size.to_s} #{type}, but loadObjects could not populate anything from deployment metadata", MU::ERR if !@object_load_fails
          @object_load_fails = true
        end
        return nil
      end
    end
    matches = {}
    @kittens[type].each { |habitat_group, sib_classes|
      next if habitat and habitat_group and habitat_group != habitat
      sib_classes.each_pair { |sib_class, cloud_objs|

        if attrs[:has_multiples]
          next if !name.nil? and name != sib_class or cloud_objs.empty?
          if !name.nil?
            if return_all
              matches.merge!(cloud_objs.clone)
              next
            elsif cloud_objs.size == 1 and does_match.call(cloud_objs.values.first)
              return cloud_objs.values.first
            end
          end
          
          cloud_objs.each_value { |obj|
            if does_match.call(obj)
              if return_all
                matches.merge!(cloud_objs.clone)
              else
                return obj.clone
              end
            end
          }
        # has_multiples is false, "cloud_objs" is actually a singular object
        elsif (name.nil? and does_match.call(cloud_objs)) or [sib_class, cloud_objs.virtual_name(name)].include?(name.to_s)
          matches[cloud_objs.config['name']] = cloud_objs.clone
        end
      }
    }

    return matches if return_all and matches.size >= 1

    return matches.values.first if matches.size == 1

  }

  return nil
end
getResourceName(name, max_length: 255, need_unique_string: false, use_unique_string: nil, reuse_unique_string: false, scrub_mu_isms: @original_config['scrub_mu_isms'], disallowed_chars: nil, never_gen_unique: false) click to toggle source

Generate a name string for a resource, incorporate the MU identifier for this deployment. Will dynamically shorten the name to fit for restrictive uses (e.g. Windows local hostnames, Amazon Elastic Load Balancers). @param name [String]: The shorthand name of the resource, usually the value of the “name” field in an Mu resource declaration. @param max_length [Integer]: The maximum length of the resulting resource name. @param need_unique_string [Boolean]: Whether to forcibly append a random three-character string to the name to ensure it's unique. Note that this behavior will be automatically invoked if the name must be truncated. @param scrub_mu_isms [Boolean]: Don't bother with generating names specific to this deployment. Used to generate generic CloudFormation templates, amongst other purposes. @param disallowed_chars [Regexp]: A pattern of characters that are illegal for this resource name, such as +/[^a-zA-Z0-9-]/+ @return [String]: A full name string for this resource

# File modules/mu/mommacat/naming.rb, line 167
def getResourceName(name, max_length: 255, need_unique_string: false, use_unique_string: nil, reuse_unique_string: false, scrub_mu_isms: @original_config['scrub_mu_isms'], disallowed_chars: nil, never_gen_unique: false)
  if name.nil?
    raise MuError, "Got no argument to MU::MommaCat.getResourceName"
  end
  if @appname.nil? or @environment.nil? or @timestamp.nil? or @seed.nil?
    MU.log "getResourceName: Missing global deploy variables in thread #{Thread.current.object_id}, using bare name '#{name}' (appname: #{@appname}, environment: #{@environment}, timestamp: #{@timestamp}, seed: #{@seed}, deploy_id: #{@deploy_id}", MU::WARN, details: caller
    return name
  end
  need_unique_string = false if scrub_mu_isms

  muname = nil
  if need_unique_string
    reserved = 4
  else
    reserved = 0
  end

  # First, pare down the base name string until it will fit
  basename = @appname.upcase + "-" + @environment.upcase + "-" + @timestamp + "-" + @seed.upcase + "-" + name.upcase
  if scrub_mu_isms
    basename = @appname.upcase + "-" + @environment.upcase + name.upcase
  end

  subchar = if disallowed_chars
    if "-".match(disallowed_chars)
      if !"_".match(disallowed_chars)
        "_"
      else
        ""
      end
    else
      "-"
    end
  end

  if disallowed_chars
    basename.gsub!(disallowed_chars, subchar) if disallowed_chars
  end
  attempts = 0
  begin
    if (basename.length + reserved) > max_length
      MU.log "Stripping name down from #{basename}[#{basename.length.to_s}] (reserved: #{reserved.to_s}, max_length: #{max_length.to_s})", MU::DEBUG
      if basename == @appname.upcase + "-" + @seed.upcase + "-" + name.upcase
        # If we've run out of stuff to strip, truncate what's left and
        # just leave room for the deploy seed and uniqueness string. This
        # is the bare minimum, and probably what you'll see for most Windows
        # hostnames.
        basename = name.upcase + "-" + @appname.upcase
        basename.slice!((max_length-(reserved+3))..basename.length)
        basename.sub!(/-$/, "")
        basename = basename + "-" + @seed.upcase
        basename.gsub!(disallowed_chars, subchar) if disallowed_chars
      else
        # If we have to strip anything, assume we've lost uniqueness and
        # will have to compensate with #genUniquenessString.
        need_unique_string = true if !never_gen_unique
        reserved = 4
        basename.sub!(/-[^-]+-#{@seed.upcase}-#{Regexp.escape(name.upcase)}$/, "")
        basename = basename + "-" + @seed.upcase + "-" + name.upcase
        basename.gsub!(disallowed_chars, subchar) if disallowed_chars
      end
    end
    attempts += 1
    raise MuError, "Failed to generate a reasonable name getResourceName(#{name}, max_length: #{max_length.to_s}, need_unique_string: #{need_unique_string.to_s}, use_unique_string: #{use_unique_string.to_s}, reuse_unique_string: #{reuse_unique_string.to_s}, scrub_mu_isms: #{scrub_mu_isms.to_s}, disallowed_chars: #{disallowed_chars})" if attempts > 10
  end while (basename.length + reserved) > max_length

  # Finally, apply our short random differentiator, if it's needed.
  if need_unique_string
    # Preferentially use a requested one, if it's not already in use.
    if !use_unique_string.nil?
      muname = basename + "-" + use_unique_string
      if !allocateUniqueResourceName(muname) and !reuse_unique_string
        MU.log "Requested to use #{use_unique_string} as differentiator when naming #{name}, but the name #{muname} is unavailable.", MU::WARN
        muname = nil
      end
    end
    if !muname
      begin
        unique_string = MU::MommaCat.genUniquenessString
        muname = basename + "-" + unique_string
      end while !allocateUniqueResourceName(muname)
      MU::MommaCat.unique_map_semaphore.synchronize {
        MU::MommaCat.name_unique_str_map[muname] = unique_string
      }
    end
  else
    muname = basename
  end
  muname.gsub!(disallowed_chars, subchar) if disallowed_chars

  return muname
end
groomNode(cloud_id, name, type, mu_name: nil, reraise_fail: false, sync_wait: true) click to toggle source

Run {MU::Cloud::Server#postBoot} and {MU::Cloud::Server#groom} on a node. @param cloud_id [OpenStruct]: The cloud provider's identifier for this node. @param name [String]: The MU resource name of the node being created. @param mu_name [String]: The full #{MU::MommaCat.getResourceName} name of the server we're grooming, if it's been initialized already. @param type [String]: The type of resource that created this node (either server or serverpool).

# File modules/mu/mommacat/daemon.rb, line 54
def groomNode(cloud_id, name, type, mu_name: nil, reraise_fail: false, sync_wait: true)
  if cloud_id.nil?
    raise GroomError, "MU::MommaCat.groomNode requires a {MU::Cloud::Server} object"
  end
  if name.nil? or name.empty?
    raise GroomError, "MU::MommaCat.groomNode requires a resource name"
  end
  if type.nil? or type.empty?
    raise GroomError, "MU::MommaCat.groomNode requires a resource type"
  end

  if !MU::MommaCat.lock(cloud_id+"-mommagroom", true)
    MU.log "Instance #{cloud_id} on #{MU.deploy_id} (#{type}: #{name}) is already being groomed, ignoring this extra request.", MU::NOTICE
    MU::MommaCat.unlockAll
    if !MU::MommaCat.locks.nil? and MU::MommaCat.locks.size > 0
      puts "------------------------------"
      puts "Open flock() locks:"
      pp MU::MommaCat.locks
      puts "------------------------------"
    end
    return
  end
  loadDeploy

  # XXX this is to stop Net::SSH from killing our entire stack when it
  # throws an exception. See ECAP-139 in JIRA. Far as we can tell, it's
  # just not entirely thread safe.
  Thread.handle_interrupt(Net::SSH::Disconnect => :never) {
    begin
      Thread.handle_interrupt(Net::SSH::Disconnect => :immediate) {
        MU.log "(Probably harmless) Caught a Net::SSH::Disconnect in #{Thread.current.inspect}", MU::DEBUG, details: Thread.current.backtrace
      }
    ensure
    end
  }

  if @original_config[type+"s"].nil?
    raise GroomError, "I see no configured resources of type #{type} (bootstrap request for #{name} on #{@deploy_id})"
  end
  kitten = nil

  kitten = findLitterMate(type: "server", name: name, mu_name: mu_name, cloud_id: cloud_id)
  if !kitten.nil?
    MU.log "Re-grooming #{mu_name}", details: kitten.deploydata
  else
    first_groom = true
    @original_config[type+"s"].each { |svr|
      if svr['name'] == name
        svr["instance_id"] = cloud_id

        # This will almost always be true in server pools, but lets be safe. Somewhat problematic because we are only
        # looking at deploy_id, but we still know this is our DNS record and not a custom one.
        if svr['dns_records'] && !svr['dns_records'].empty?
          svr['dns_records'].each { |dnsrec|
            if dnsrec.has_key?("name") && dnsrec['name'].start_with?(MU.deploy_id.downcase)
              MU.log "DNS record for #{MU.deploy_id.downcase}, #{name} is probably wrong, deleting", MU::WARN, details: dnsrec
              dnsrec.delete('name')
              dnsrec.delete('target')
            end
          }
        end

        kitten = MU::Cloud::Server.new(mommacat: self, kitten_cfg: svr, cloud_id: cloud_id)
        mu_name = kitten.mu_name if mu_name.nil?
        MU.log "Grooming #{mu_name} for the first time", details: svr
        break
      end
    }
  end

  begin
    # This is a shared lock with MU::Cloud::AWS::Server.create, to keep from
    # stomping on synchronous deploys that are still running. This
    # means we're going to wait here if this instance is still being
    # bootstrapped by "regular" means.
    if !MU::MommaCat.lock(cloud_id+"-create", true)
      MU.log "#{mu_name} is still in mid-creation, skipping", MU::NOTICE
      MU::MommaCat.unlockAll
      if !MU::MommaCat.locks.nil? and MU::MommaCat.locks.size > 0
        puts "------------------------------"
        puts "Open flock() locks:"
        pp MU::MommaCat.locks
        puts "------------------------------"
      end
      return
    end
    MU::MommaCat.unlock(cloud_id+"-create")

    if !kitten.postBoot(cloud_id)
      MU.log "#{mu_name} is already being groomed, skipping", MU::NOTICE
      MU::MommaCat.unlockAll
      if !MU::MommaCat.locks.nil? and MU::MommaCat.locks.size > 0
        puts "------------------------------"
        puts "Open flock() locks:"
        pp MU::MommaCat.locks
        puts "------------------------------"
      end
      return
    end

    # This is a shared lock with MU::Deploy.createResources, simulating the
    # thread logic that tells MU::Cloud::AWS::Server.deploy to wait until
    # its dependencies are ready. We don't, for example, want to start
    # deploying if we rely on an RDS instance that isn't ready yet. We can
    # release this immediately, once we successfully grab it.
    MU::MommaCat.lock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies")
    MU::MommaCat.unlock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies")

    kitten.groom
  rescue StandardError => e
    MU::MommaCat.unlockAll
    if e.class.name != "MU::Cloud::AWS::Server::BootstrapTempFail" and !File.exist?(deploy_dir+"/.cleanup."+cloud_id) and !File.exist?(deploy_dir+"/.cleanup")
      MU.log "Grooming FAILED for #{kitten.mu_name} (#{e.inspect})", MU::ERR, details: e.backtrace
      sendAdminSlack("Grooming FAILED for `#{kitten.mu_name}` with `#{e.message}` :crying_cat_face:", msg: e.backtrace.join("\n"))
      sendAdminMail("Grooming FAILED for #{kitten.mu_name} on #{MU.appname} \"#{MU.handle}\" (#{MU.deploy_id})",
        msg: e.inspect,
        data: e.backtrace,
        debug: true
      )
      raise e if reraise_fail
    else
      MU.log "Grooming of #{kitten.mu_name} interrupted by cleanup or planned reboot"
    end
    return
  end

  if !@deployment['servers'].nil? and !sync_wait
    syncLitter(@deployment["servers"].keys, triggering_node: kitten)
  end
  MU::MommaCat.unlock(cloud_id+"-mommagroom")
  if MU.myCloud == "AWS"
    MU::Cloud::AWS.openFirewallForClients # XXX add the other clouds, or abstract
  end
  MU::MommaCat.getLitter(MU.deploy_id)
  MU::Master.syncMonitoringConfig(false)
  MU.log "Grooming complete for '#{name}' mu_name on \"#{MU.handle}\" (#{MU.deploy_id})"
  FileUtils.touch(MU.dataDir+"/deployments/#{MU.deploy_id}/#{name}_done.txt")
  MU::MommaCat.unlockAll
  if first_groom
    sendAdminSlack("Grooming complete for #{mu_name} :heart_eyes_cat:")
    sendAdminMail("Grooming complete for '#{name}' (#{mu_name}) on deploy \"#{MU.handle}\" (#{MU.deploy_id})")
  end
  return
end
habitatsUsed() click to toggle source

List the accounts/projects/subscriptions used by each resource in our deploy. @return [Array<String>]

# File modules/mu/mommacat.rb, line 271
def habitatsUsed
  return [] if !@original_config
  habitats = []
  habitats << @original_config['project'] if @original_config['project']
  if @original_config['habitat']
    hab_ref = MU::Config::Ref.get(@original_config['habitat'])
    if hab_ref and hab_ref.id
      habitats << hab_ref.id
    end
  end

  MU::Cloud.resource_types.each_value { |attrs|
    type = attrs[:cfg_plural]
    if @original_config[type]
      @original_config[type].each { |resource|
        if resource['project']
          habitats << resource['project']
        elsif resource['habitat']
          hab_ref = MU::Config::Ref.get(resource['habitat'])
          if hab_ref and hab_ref.id
            habitats << hab_ref.id
          end
        elsif resource['cloud']
          # XXX this should be a general method implemented by each cloud
          # provider
          if resource['cloud'] == "Google"
            habitats << MU::Cloud.cloudClass(resource['cloud']).defaultProject(resource['credentials'])
          end
        end
      }
    end
  }

  habitats.uniq!
end
listNodes() click to toggle source

Return a list of all nodes associated with the current deployment. @return [Hash]

# File modules/mu/mommacat.rb, line 728
def listNodes
  nodes = Hash.new
  if !@deployment['servers'].nil?
    @deployment['servers'].each_pair { |nodetype, node|
      node.each_pair { |name, metadata|
        if name.nil? or metadata.nil? or !metadata.is_a?(Hash)
          MU.log "Original config of deploy #{MU.deploy_id} looks funny. It's probably very old.", MU::WARN
          next
        end
        metadata['deploy_id'] = MU.deploy_id
        nodes[name] = metadata
        ['servers', 'server_pools'].each { |res_type|
          if !@original_config[res_type].nil?
            @original_config[res_type].each { |srv_conf|
              if srv_conf['name'] == nodetype
                nodes[name]['conf'] = srv_conf.dup
              end
            }
          end
        }
      }
    }
  end

  return nodes
end
listStandardTags() click to toggle source

List the name/value pairs for our mandatory standard set of resource tags for this deploy. @return [Hash<String,String>]

# File modules/mu/mommacat/naming.rb, line 275
def listStandardTags
  {
    "MU-ID" => @deploy_id,
    "MU-APP" => @appname,
    "MU-ENV" => @environment,
    "MU-MASTER-IP" => MU.mu_public_ip
  }
end
nodeSSLCerts(resource, poolname = false, keysize = 4096) click to toggle source

Given a MU::Cloud object, return the generic self-signed SSL certficate we made for it. If one doesn't exist yet, generate it first. If it's a Windows node, also generate a certificate for WinRM client auth. @param resource [MU::Cloud]: The server or other MU::Cloud resource object for which to generate or return the cert @param poolname [Boolean]: If true, generate certificates for the base name of the server pool of which this node is a member, rather than for the individual node @param keysize [Integer]: The size of the private key to use when generating this certificate

# File modules/mu/mommacat.rb, line 859
def nodeSSLCerts(resource, poolname = false, keysize = 4096)
  _nat_ssh_key, _nat_ssh_user, _nat_ssh_host, canonical_ip, _ssh_user, _ssh_key_name = resource.getSSHConfig if resource.respond_to?(:getSSHConfig)

  deploy_id = resource.deploy_id || @deploy_id || resource.deploy.deploy_id

  cert_cn = poolname ? deploy_id + "-" + resource.config['name'].upcase : resource.mu_name

  results = {}

  is_windows = (resource.respond_to?(:windows?) and resource.windows?)

  @node_cert_semaphore.synchronize {
    MU::Master::SSL.bootstrap
    sans = []
    sans << canonical_ip if canonical_ip
    sans << resource.mu_name.downcase if resource.mu_name and resource.mu_name != cert_cn
    # XXX were there other names we wanted to include?
    key = MU::Master::SSL.getKey(cert_cn, keysize: keysize)
    cert, pfx_cert = MU::Master::SSL.getCert(cert_cn, "/CN=#{cert_cn}/O=Mu/C=US", sans: sans, pfx: is_windows)
    results[cert_cn] = [key, cert]

    winrm_cert = nil
    if is_windows
      winrm_key = MU::Master::SSL.getKey(cert_cn+"-winrm", keysize: keysize)
      winrm_cert = MU::Master::SSL.getCert(cert_cn+"-winrm", "/CN=#{resource.config['windows_admin_username']}/O=Mu/C=US", sans: ["otherName:1.3.6.1.4.1.311.20.2.3;UTF8:#{resource.config['windows_admin_username']}@localhost"], pfx: true)[0]
      results[cert_cn+"-winrm"] = [winrm_key, winrm_cert]
    end

    if resource and resource.config and resource.config['cloud']
      cloudclass = MU::Cloud.cloudClass(resource.config['cloud'])

      cloudclass.writeDeploySecret(self, cert.to_pem, cert_cn+".crt", credentials: resource.config['credentials'])
      cloudclass.writeDeploySecret(self, key.to_pem, cert_cn+".key", credentials: resource.config['credentials'])
      if pfx_cert
        cloudclass.writeDeploySecret(self, pfx_cert.to_der, cert_cn+".pfx", credentials: resource.config['credentials'])
      end
      if winrm_cert
        cloudclass.writeDeploySecret(self, winrm_cert.to_pem, cert_cn+"-winrm.crt", credentials: resource.config['credentials'])
      end
    end

  }

  results[cert_cn]
end
notify(type, key, data, mu_name: nil, remove: false, triggering_node: nil, delayed_save: false) click to toggle source

Add or remove a resource's metadata to this deployment's structure and flush it to disk. @param type [String]: The type of resource (e.g. server, database). @param key [String]: The name field of this resource. @param mu_name [String]: The mu_name of this resource. @param data [Hash]: The resource's metadata. @param triggering_node [MU::Cloud]: A cloud object calling this notify, usually on behalf of itself @param remove [Boolean]: Remove this resource from the deploy structure, instead of adding it. @return [void]

# File modules/mu/mommacat.rb, line 552
def notify(type, key, data, mu_name: nil, remove: false, triggering_node: nil, delayed_save: false)
  no_write = (@no_artifacts or !caller.grep(/\/mommacat\.rb:\d+:in `notify'/).empty?)

  begin
    if !no_write
      if !MU::MommaCat.lock("deployment-notification", deploy_id: @deploy_id, retries: 300)
        raise MuError, "Failed to get deployment-notifcation lock for #{@deploy_id}"
      end
    end

    if !@need_deploy_flush or @deployment.nil? or @deployment.empty?
      loadDeploy(true) # make sure we're saving the latest and greatest
    end

    @timestamp ||= @deployment['timestamp']
    @seed ||= @deployment['seed']
    @appname ||= @deployment['appname']
    @handle ||= @deployment['handle']
    
    _shortclass, _cfg_name, mu_type, _classname, attrs = MU::Cloud.getResourceNames(type, false)
    type = mu_type if mu_type
    has_multiples = attrs[:has_multiples] ? true : false

    mu_name ||= if !data.nil? and !data["mu_name"].nil?
      data["mu_name"]
    elsif !triggering_node.nil? and !triggering_node.mu_name.nil?
      triggering_node.mu_name
    end
    if mu_name.nil? and has_multiples
      MU.log "MU::MommaCat.notify called to modify deployment struct for a type (#{type}) with :has_multiples, but no mu_name available to look under #{key}. Call was #{caller(1..1)}", MU::WARN, details: data
      return
    end

    @need_deploy_flush = true
    @last_modified = Time.now

    if !remove
      if data.nil?
        MU.log "MU::MommaCat.notify called to modify deployment struct, but no data provided", MU::WARN
        return
      end
      @notify_semaphore.synchronize {
        @deployment[type] ||= {}
      }
      if has_multiples
        @notify_semaphore.synchronize {
          @deployment[type][key] ||= {}
        }
        @deployment[type][key][mu_name] = data
        MU.log "Adding to @deployment[#{type}][#{key}][#{mu_name}]", MU::DEBUG, details: data
      else
        @deployment[type][key] = data
        MU.log "Adding to @deployment[#{type}][#{key}]", MU::DEBUG, details: data
      end
      if !delayed_save and !no_write
        save!(key)
      end
    else
      have_deploy = true
      if @deployment[type].nil? or @deployment[type][key].nil?
        MU.log "MU::MommaCat.notify called to remove #{type} #{key}#{has_multiples ? " "+mu_name : ""} deployment struct, but no such data exist", MU::DEBUG
        return
      end

      if have_deploy
        @notify_semaphore.synchronize {
          if has_multiples
            MU.log "Removing @deployment[#{type}][#{key}][#{mu_name}]", MU::DEBUG, details: @deployment[type][key][mu_name]
            @deployment[type][key].delete(mu_name)
          end

          if @deployment[type][key].empty? or !has_multiples
            MU.log "Removing @deployment[#{type}][#{key}]", MU::DEBUG, details: @deployment[type][key]
            @deployment[type].delete(key)
          end

          if @deployment[type].empty?
            @deployment.delete(type)
          end
        }
      end
      save! if !delayed_save and !no_write
    end
  ensure
    MU::MommaCat.unlock("deployment-notification", deploy_id: @deploy_id) if !no_write
  end
end
numKittens(clouds: [], types: [], negate: false) click to toggle source

Tell us the number of first-class resources we've configured, optionally filtering results to only include a given type and/or in a given cloud environment. @param clouds [Array<String>]: The cloud environment(s) to check for. If unspecified, will match all environments in this deployment. @param types [Array<String>]: The type of resource(s) to check for. If unspecified, will match all resources in this deployment. @param negate [Boolean]: Invert logic of the other filters if they are specified, e.g. search for all cloud resources that are not AWS.

# File modules/mu/mommacat.rb, line 345
def numKittens(clouds: [], types: [], negate: false)
  realtypes = []
  return 0 if @original_config.nil?
  if !types.nil? and types.size > 0
    types.each { |type|
      cfg_plural = MU::Cloud.getResourceNames(type)[2]
      realtypes << cfg_plural
    }
  end

  count = 0
  MU::Cloud.resource_types.each_value { |data|
    next if @original_config[data[:cfg_plural]].nil?
    next if realtypes.size > 0 and (!negate and !realtypes.include?(data[:cfg_plural]))
    @original_config[data[:cfg_plural]].each { |resource|
      if clouds.nil? or clouds.size == 0 or (!negate and clouds.include?(resource["cloud"])) or (negate and !clouds.include?(resource["cloud"]))
        count = count + 1
      end
    }
  }
  count
end
purge!() click to toggle source

Remove the metadata of the currently loaded deployment.

# File modules/mu/mommacat/storage.rb, line 305
def purge!
  MU::MommaCat.purge(MU.deploy_id)
end
regionsUsed() click to toggle source

List the regions used by each resource in our deploy. This will just be a flat list of strings with no regard to which region belongs with what cloud provider- things mostly use this as a lookup table so they can safely skip unnecessary regions when creating/cleaning deploy artifacts. @return [Array<String>]

# File modules/mu/mommacat.rb, line 312
    def regionsUsed
      return [] if !@original_config
      regions = []
      regions << @original_config['region'] if @original_config['region']
      MU::Cloud.resource_types.each_pair { |res_type, attrs|
        type = attrs[:cfg_plural]
        if @original_config[type]
          @original_config[type].each { |resource|
            if resource['cloud']
              if MU::Cloud.resourceClass(resource['cloud'], res_type).isGlobal?
# XXX why was I doing this, urgh
                next
              elsif !resource['region']
                regions << MU::Cloud.cloudClass(resource['cloud']).myRegion(resource['credentials'])
              end
            end
            if resource['region']
              regions << resource['region'] if resource['region']
            else
            end
          }
        end
      }

      regions.uniq
    end
removeKitten(object) click to toggle source

@param object [MU::Cloud]:

# File modules/mu/mommacat.rb, line 369
def removeKitten(object)
  if !object
    raise MuError, "Nil arguments to removeKitten are not allowed"
  end
  @kitten_semaphore.synchronize {
    MU::Cloud.resource_types.each_value { |attrs|
      type = attrs[:cfg_plural]
      next if !@kittens.has_key?(type)
      tmplitter = @kittens[type].values.dup
      tmplitter.each { |nodeclass, data|
        if data.is_a?(Hash)
          data.each_key { |mu_name|
            if data == object
              @kittens[type][nodeclass].delete(mu_name)
              return
            end
          }
        else
          if data == object
            @kittens[type].delete(nodeclass)
            return
          end
        end
      }
    }
  }
  @kittens
end
retrieveWindowsAdminCreds(server) click to toggle source

For a given (Windows) server, return it's administrator user and password. This is generally for requests made to MommaCat from said server, which we can assume have been authenticated with the deploy secret. @param server [MU::Cloud::Server]: The Server object whose credentials we're fetching.

# File modules/mu/mommacat.rb, line 759
def retrieveWindowsAdminCreds(server)
  if server.nil?
    raise MuError, "retrieveWindowsAdminCreds must be called with a Server object"
  elsif !server.is_a?(MU::Cloud::Server)
    raise MuError, "retrieveWindowsAdminCreds must be called with a Server object (got #{server.class.name})"
  end
  if server.config['use_cloud_provider_windows_password']
    return [server.config["windows_admin_username"], server.getWindowsAdminPassword]
  elsif server.config['windows_auth_vault'] && !server.config['windows_auth_vault'].empty?
    if server.config["windows_auth_vault"].has_key?("password_field")
      return [server.config["windows_admin_username"],
        server.groomer.getSecret(
          vault: server.config['windows_auth_vault']['vault'],
          item: server.config['windows_auth_vault']['item'],
          field: server.config["windows_auth_vault"]["password_field"]
        )]
    else
      return [server.config["windows_admin_username"], server.getWindowsAdminPassword]
    end
  end
  []
end
save!(triggering_node = nil, force: false, origin: nil) click to toggle source

Synchronize all in-memory information related to this to deployment to disk. @param triggering_node [MU::Cloud::Server]: If we're being triggered by the addition/removal/update of a node, this allows us to notify any sibling or dependent nodes of changes @param force [Boolean]: Save even if no_artifacts is set @param origin [Hash]: Optional blob of data indicating how this deploy was created

# File modules/mu/mommacat/storage.rb, line 382
def save!(triggering_node = nil, force: false, origin: nil)

  return if @no_artifacts and !force

  MU::MommaCat.deploy_struct_semaphore.synchronize {
    MU.log "Saving deployment #{MU.deploy_id}", MU::DEBUG

    if !Dir.exist?(deploy_dir)
      MU.log "Creating #{deploy_dir}", MU::DEBUG
      Dir.mkdir(deploy_dir, 0700)
    end

    writeFile("origin.json", JSON.pretty_generate(origin)) if !origin.nil?
    writeFile("private_key", @private_key) if !@private_key.nil?
    writeFile("public_key", @public_key) if !@public_key.nil?

    if !@deployment.nil? and @deployment.size > 0
      @deployment['handle'] = MU.handle if @deployment['handle'].nil? and !MU.handle.nil?
      [:public_key, :timestamp, :seed, :appname, :handle, :ssh_public_key].each { |var|
        value = instance_variable_get(("@"+var.to_s).to_sym)
        @deployment[var.to_s] = value if value
      }
      
      begin
        # XXX doing this to trigger JSON errors before stomping the stored
        # file...
        JSON.pretty_generate(@deployment, max_nesting: false)
        deploy = File.new("#{deploy_dir}/deployment.json", File::CREAT|File::TRUNC|File::RDWR, 0600)
        MU.log "Getting lock to write #{deploy_dir}/deployment.json", MU::DEBUG
        deploy.flock(File::LOCK_EX)
        deploy.puts JSON.pretty_generate(@deployment, max_nesting: false)
      rescue JSON::NestingError => e
        MU.log e.inspect, MU::ERR, details: @deployment
        raise MuError, "Got #{e.message} trying to save deployment"
      rescue Encoding::UndefinedConversionError => e
        MU.log e.inspect, MU::ERR, details: @deployment
        raise MuError, "Got #{e.message} at #{e.error_char.dump} (#{e.source_encoding_name} => #{e.destination_encoding_name}) trying to save deployment"
      end
      deploy.flock(File::LOCK_UN)
      deploy.close
      @need_deploy_flush = false
      @last_modified = nil
      MU::MommaCat.updateLitter(@deploy_id, self)
    end

    if !@original_config.nil? and @original_config.is_a?(Hash)
      writeFile("basket_of_kittens.json", JSON.pretty_generate(MU::Config.manxify(@original_config)))
    end

    writeFile("node_ssh.key", @ssh_private_key) if !@ssh_private_key.nil?
    writeFile("node_ssh.pub", @ssh_public_key) if !@ssh_public_key.nil?
    writeFile("ssh_key_name", @ssh_key_name) if !@ssh_key_name.nil?
    writeFile("environment_name", @environment) if !@environment.nil?
    writeFile("deploy_secret", @deploy_secret) if !@deploy_secret.nil?

    if !@secrets.nil?
      secretdir = "#{deploy_dir}/secrets"
      if !Dir.exist?(secretdir)
        MU.log "Creating #{secretdir}", MU::DEBUG
        Dir.mkdir(secretdir, 0700)
      end
      @secrets.each_pair { |type, servers|
        servers.each_pair { |server, svr_secret|
          writeFile("secrets/#{type}.#{server}", svr_secret)
        }
      }
    end
  }

  # Update groomer copies of this metadata
  syncLitter(@deployment['servers'].keys, triggering_node: triggering_node, save_only: true) if @deployment.has_key?("servers")
end
saveNodeSecret(instance_id, raw_secret, type) click to toggle source

Save a string into deployment metadata for the current deployment, encrypting it with our deploy key. @param instance_id [String]: The cloud instance identifier with which this secret is associated. @param raw_secret [String]: The unencrypted string to store. @param type [String]: The type of secret, used to identify for retrieval.

# File modules/mu/mommacat.rb, line 455
def saveNodeSecret(instance_id, raw_secret, type)
  return if @no_artifacts
  if instance_id.nil? or instance_id.empty? or raw_secret.nil? or raw_secret.empty? or type.nil? or type.empty?
    raise SecretError, "saveNodeSecret requires instance_id (#{instance_id}), raw_secret (#{raw_secret}), and type (#{type}) args"
  end
  MU::MommaCat.lock("deployment-notification")
  loadDeploy(true) # make sure we're not trampling deployment data
  @secret_semaphore.synchronize {
    if @secrets[type].nil?
      raise SecretError, "'#{type}' is not a valid secret type (valid types: #{@secrets.keys.join(", ")})"
    end
    @secrets[type][instance_id] = encryptWithDeployKey(raw_secret)
  }
  save!
  MU::MommaCat.unlock("deployment-notification")
end
sendAdminMail(subject, msg: "", kitten: nil, data: nil, debug: false) click to toggle source

Send an email notification to a deployment's administrators. @param subject [String]: The subject line of the message. @param msg [String]: The message body. @param data [Array]: Supplemental data to add to the message body. @param debug [Boolean]: If set, will include the full deployment structure and original {MU::Config}-parsed configuration. @return [void]

# File modules/mu/mommacat.rb, line 683
    def sendAdminMail(subject, msg: "", kitten: nil, data: nil, debug: false)
      require 'net/smtp'
      if @deployment.nil?
        MU.log "Can't send admin mail without a loaded deployment", MU::ERR
        return
      end
      to = Array.new
      if !@original_config.nil?
        @original_config['admins'].each { |admin|
          to << "#{admin['name']} <#{admin['email']}>"
        }
      end
      message = <<MAIL_HEAD_END
From: #{MU.handle} <root@localhost>
To: #{to.join(",")}
Subject: #{subject}

      #{msg}
MAIL_HEAD_END
      if !kitten.nil? and kitten.kind_of?(MU::Cloud)
        message = message + "\n\n**** #{kitten}:\n"
        if !kitten.report.nil?
          kitten.report.each { |line|
            message = message + line
          }
        end
      end
      if !data.nil?
        message = message + "\n\n" + PP.pp(data, "")
      end
      if debug
        message = message + "\n\n**** Stack configuration:\n" + PP.pp(@original_config, "")
        message = message + "\n\n**** Deployment structure:\n" + PP.pp(@deployment, "")
      end
      begin
        Net::SMTP.start('localhost') do |smtp|
          smtp.send_message message, "root@localhost", to
        end
      rescue Net::SMTPFatalError, Errno::ECONNREFUSED => e
        MU.log e.inspect, MU::WARN
      end
    end
sendAdminSlack(subject, msg: "", scrub_mu_isms: true, snippets: [], noop: false) click to toggle source

Send a Slack notification to a deployment's administrators. @param subject [String]: The subject line of the message. @param msg [String]: The message body. @return [void]

# File modules/mu/mommacat.rb, line 644
def sendAdminSlack(subject, msg: "", scrub_mu_isms: true, snippets: [], noop: false)
  if MU.muCfg['slack'] and MU.muCfg['slack']['webhook'] and
     (!MU.muCfg['slack']['skip_environments'] or !MU.muCfg['slack']['skip_environments'].any?{ |s| s.casecmp(MU.environment)==0 })
    require 'slack-notifier'
    slackargs = nil
    keyword_args = { channel: MU.muCfg['slack']['channel'] }
    begin
      slack = Slack::Notifier.new MU.muCfg['slack']['webhook']
      prefix = scrub_mu_isms ? subject : "#{MU.appname} \*\"#{MU.handle}\"\* (`#{MU.deploy_id}`) - #{subject}"

      text = if msg and !msg.empty?
        "#{prefix}:\n\n```#{msg}```"
      else
        prefix
      end

      if snippets and snippets.size > 0
        keyword_args[:attachments] = snippets
      end

      if !noop
        slack.ping(text, **keyword_args)
      else
        MU.log "Would send to #{MU.muCfg['slack']['channel']}", MU::NOTICE, details: [ text, keyword_args ]
      end
    rescue Slack::Notifier::APIError => e
      MU.log "Failed to send message to slack: #{e.message}", MU::ERR, details: keyword_args
      return false
    end
  end
  true
end
signSSLCert(csr_path, sans = []) click to toggle source

Given a Certificate Signing Request, sign it with our internal CA and write the resulting signed certificate. Only works on local files. @param csr_path [String]: The CSR to sign, as a file.

# File modules/mu/mommacat.rb, line 785
def signSSLCert(csr_path, sans = [])
  MU::Master::SSL.sign(csr_path, sans, for_user: MU.mu_user)
end
syncLitter(nodeclasses = [], triggering_node: nil, save_only: false) click to toggle source

Make sure deployment data is synchronized to/from each Server in the currently-loaded deployment. @param nodeclasses [Array<String>] @param triggering_node [String,MU::Cloud::Server] @param save_only [Boolean]

# File modules/mu/mommacat.rb, line 794
def syncLitter(nodeclasses = [], triggering_node: nil, save_only: false)
  return if MU.syncLitterThread # don't run recursively by accident
  return if !Dir.exist?(deploy_dir)

  if !triggering_node.nil? and triggering_node.is_a?(MU::Cloud::Server)
    triggering_node = triggering_node.mu_name
  end

  siblings = findLitterMate(type: "server", return_all: true)
  return if siblings.nil? or (siblings.respond_to?(:empty?) and siblings.empty?)

  update_servers = []
  siblings.each_pair { |mu_name, node|
    next if mu_name == triggering_node or node.groomer.nil?
    next if nodeclasses.size > 0 and !nodeclasses.include?(node.config['name'])
    if !node.deploydata or !node.deploydata['nodename']
      MU.log "#{mu_name} deploy data is missing (possibly retired or mid-bootstrap), so not syncing it", MU::NOTICE
      next
    end

    if @deployment["servers"][node.config['name']][node.mu_name].nil? or
       @deployment["servers"][node.config['name']][node.mu_name] != node.deploydata
      @deployment["servers"][node.config['name']][node.mu_name] = node.deploydata
    elsif !save_only
      # Don't bother running grooms on nodes that don't need to be updated,
      # unless we're just going to do a save.
      next
    end
    update_servers << node
  }

  return if update_servers.empty?

  MU.log "Updating nodes in #{@deploy_id}", MU::DEBUG, details: update_servers.map { |n| n.mu_name }

  threads = []
  update_servers.each { |sibling|
    next if sibling.config.has_key?("groom") and !sibling.config["groom"]
    threads << Thread.new {
      Thread.abort_on_exception = true
      Thread.current.thread_variable_set("name", "sync-"+sibling.mu_name.downcase)
      MU.setVar("syncLitterThread", true)
      begin
        sibling.groomer.saveDeployData
        sibling.groomer.run(purpose: "Synchronizing sibling kittens") if !save_only
      rescue MU::Groomer::RunError => e
        MU.log "Sync of #{sibling.mu_name} failed", MU::WARN, details: e.inspect
      end
    }
  }

  threads.each { |t|
    t.join
  }

  MU.log "Synchronization of #{@deploy_id} complete", MU::DEBUG, details: update_servers
end
updateBasketofKittens(new_conf, skip_validation: false, new_metadata: nil, save_now: false) click to toggle source

Overwrite this deployment's configuration with a new version. Save the previous version as well. @param new_conf [Hash]: A new configuration, fully resolved by {MU::Config}

# File modules/mu/mommacat/storage.rb, line 95
    def updateBasketofKittens(new_conf, skip_validation: false, new_metadata: nil, save_now: false)
      loadDeploy
      if new_conf == @original_config
        return
      end

      scrub_with = nil

      # Make sure the new config that we were just handed resolves and makes
      # sense
      if !skip_validation
        f = Tempfile.new(@deploy_id)
        f.write JSON.parse(JSON.generate(new_conf)).to_yaml
        conf_engine = MU::Config.new(f.path) # will throw an exception if it's bad, adoption should catch this and cope reasonably
        scrub_with = conf_engine.config
        f.close
      end

      backup = "#{deploy_dir}/basket_of_kittens.json.#{Time.now.to_i.to_s}"
      MU.log "Saving previous config of #{@deploy_id} to #{backup}"
      config = File.new(backup, File::CREAT|File::TRUNC|File::RDWR, 0600)
      config.flock(File::LOCK_EX)
      config.puts JSON.pretty_generate(@original_config)
      config.flock(File::LOCK_UN)
      config.close

      @original_config = new_conf.clone

      MU::Cloud.resource_types.each_pair { |res_type, attrs|
        next if !@deployment.has_key?(attrs[:cfg_plural])
        deletia = []
# existing_deploys
        @deployment[attrs[:cfg_plural]].each_pair { |res_name, data|
          orig_cfg = findResourceConfig(attrs[:cfg_plural], res_name, (scrub_with || @original_config))

          if orig_cfg.nil? and (!data['mu_name'] or data['mu_name'] =~ /^#{Regexp.quote(@deploy_id)}/)
            MU.log "#{res_type} #{res_name} no longer configured, will remove deployment metadata", MU::NOTICE, details: data
            deletia << res_name
          end
        }
        @deployment[attrs[:cfg_plural]].reject! { |k, v| deletia.include?(k) }
      }

      if save_now
        save!
        MU.log "New config saved to #{deploy_dir}/basket_of_kittens.json"
      end
    end
writeDeploySecret() click to toggle source

Write our shared deploy secret out to wherever the cloud provider layers like to stash it.

# File modules/mu/mommacat/storage.rb, line 548
def writeDeploySecret
  return if !@deploy_secret
  credsets = credsUsed
  return if !credsets
  if !@original_config['scrub_mu_isms'] and !@no_artifacts
    cloudsUsed.each { |cloud|
      credsets.each { |credentials|
        next if MU::Cloud.cloudClass(cloud).credConfig(credentials).nil? # XXX this is a dumb way to check this, should be able to get credsUsed by cloud
        MU::Cloud.cloudClass(cloud).writeDeploySecret(self, @deploy_secret, credentials: credentials)
      }
    }
  end
end

Private Instance Methods

allocateUniqueResourceName(name) click to toggle source

Check to see whether a given resource name is unique across all deployments on this Mu server. We only enforce this for certain classes of names. If the name in question is available, add it to our cache of said names. See #{MU::MommaCat.getResourceName} @param name [String]: The name to attempt to allocate. @return [Boolean]: True if allocation was successful.

# File modules/mu/mommacat/naming.rb, line 431
def allocateUniqueResourceName(name)
  raise MuError, "Cannot call allocateUniqueResourceName without an active deployment" if @deploy_id.nil?
  path = File.expand_path(MU.dataDir+"/deployments")
  File.open(path+"/unique_ids", File::CREAT|File::RDWR, 0600) { |f|
    existing = []
    f.flock(File::LOCK_EX)
    f.readlines.each { |line|
      existing << line.chomp
    }
    begin
      existing.each { |used|
        if used.match(/^#{name}:/)
          if !used.match(/^#{name}:#{@deploy_id}$/)
            MU.log "#{name} is already reserved by another resource on this Mu server.", MU::WARN, details: caller
            return false
          else
            return true
          end
        end
      }
      f.puts name+":"+@deploy_id
      return true
    ensure
      f.flock(File::LOCK_UN)
    end
  }
end
createDeployKey() click to toggle source
# File modules/mu/mommacat.rb, line 907
def createDeployKey
  key = OpenSSL::PKey::RSA.generate(4096)
  MU.log "Generated deploy key for #{MU.deploy_id}", MU::DEBUG, details: key.public_key.export
  return [key.export, key.public_key.export]
end
findResourceConfig(type, name, config = @original_config) click to toggle source
# File modules/mu/mommacat/storage.rb, line 731
def findResourceConfig(type, name, config = @original_config)
  orig_cfg = nil
  if config.has_key?(type)
    config[type].each { |resource|
      if resource["name"] == name
        orig_cfg = resource
        break
      end
    }
  end
  
  # Some Server objects originated from ServerPools, get their
  # configs from there
  if type == "servers" and orig_cfg.nil? and config.has_key?("server_pools")
    config["server_pools"].each { |resource|
      if resource["name"] == name
        orig_cfg = resource
        break
      end
    }
  end

  orig_cfg
end
initDeployDirectory() click to toggle source

Helper for initialize

# File modules/mu/mommacat/storage.rb, line 623
def initDeployDirectory
  if !Dir.exist?(MU.dataDir+"/deployments")
    MU.log "Creating #{MU.dataDir}/deployments", MU::DEBUG
    Dir.mkdir(MU.dataDir+"/deployments", 0700)
  end
  path = File.expand_path(MU.dataDir+"/deployments")+"/"+@deploy_id
  if !Dir.exist?(path)
    MU.log "Creating #{path}", MU::DEBUG
    Dir.mkdir(path, 0700)
  end

  @ssh_key_name, @ssh_private_key, @ssh_public_key = self.SSHKey
  if !File.exist?(deploy_dir+"/private_key")
    @private_key, @public_key = createDeployKey
  end

end
loadDeploy(deployment_json_only = false, set_context_to_me: true) click to toggle source
# File modules/mu/mommacat/storage.rb, line 681
def loadDeploy(deployment_json_only = false, set_context_to_me: true)
  MU::MommaCat.deploy_struct_semaphore.synchronize {
    success = loadDeployFromCache(set_context_to_me)

    @timestamp ||= @deployment['timestamp']
    @seed ||= @deployment['seed']
    @appname ||= @deployment['appname']
    @handle ||= @deployment['handle']

    return if deployment_json_only and success

    if File.exist?(deploy_dir+"/private_key")
      @private_key = File.read("#{deploy_dir}/private_key")
      @public_key = File.read("#{deploy_dir}/public_key")
    end

    if File.exist?(deploy_dir+"/basket_of_kittens.json")
      begin
        @original_config = JSON.parse(File.read("#{deploy_dir}/basket_of_kittens.json"))
      rescue JSON::ParserError => e
        MU.log "JSON parse failed on #{deploy_dir}/basket_of_kittens.json", MU::ERR, details: e.message
      end
    end
    if File.exist?(deploy_dir+"/ssh_key_name")
      @ssh_key_name = File.read("#{deploy_dir}/ssh_key_name").chomp!
    end
    if File.exist?(deploy_dir+"/node_ssh.key")
      @ssh_private_key = File.read("#{deploy_dir}/node_ssh.key")
    end
    if File.exist?(deploy_dir+"/node_ssh.pub")
      @ssh_public_key = File.read("#{deploy_dir}/node_ssh.pub")
    end
    if File.exist?(deploy_dir+"/environment_name")
      @environment = File.read("#{deploy_dir}/environment_name").chomp!
    end
    if File.exist?(deploy_dir+"/deploy_secret")
      @deploy_secret = File.read("#{deploy_dir}/deploy_secret")
    end
    if Dir.exist?("#{deploy_dir}/secrets")
      @secrets.each_key { |type|
        Dir.glob("#{deploy_dir}/secrets/#{type}.*") { |filename|
          server = File.basename(filename).split(/\./)[1]

          @secrets[type][server] = File.read(filename).chomp!
        }
      }
    end
  }
end
loadDeployFromCache(set_context_to_me = true) click to toggle source
# File modules/mu/mommacat/storage.rb, line 643
    def loadDeployFromCache(set_context_to_me = true)
      return false if !File.size?(deploy_dir+"/deployment.json")

      lastmod = File.mtime("#{deploy_dir}/deployment.json")
      if @last_modified and lastmod < @last_modified
        MU.log "#{deploy_dir}/deployment.json last written at #{lastmod}, live meta at #{@last_modified}, not loading", MU::WARN if @last_modified
        # this is a weird place for this
        setThreadContextToMe if set_context_to_me
        return true
      end

      deploy = File.open("#{deploy_dir}/deployment.json", File::RDONLY)
      MU.log "Getting lock to read #{deploy_dir}/deployment.json", MU::DEBUG
      # deploy.flock(File::LOCK_EX)
      begin
        Timeout::timeout(90) {deploy.flock(File::LOCK_EX)}
      rescue Timeout::Error
        raise MuError, "Timed out trying to get an exclusive lock on #{deploy_dir}/deployment.json"
      end

      begin
        @deployment = JSON.parse(File.read("#{deploy_dir}/deployment.json"))
# XXX is it worthwhile to merge fuckery?
      rescue JSON::ParserError => e
        MU.log "JSON parse failed on #{deploy_dir}/deployment.json", MU::ERR, details: e.message
      end

      deploy.flock(File::LOCK_UN)
      deploy.close

      setThreadContextToMe if set_context_to_me

      true
    end
loadObjects(delay_descriptor_load) click to toggle source
# File modules/mu/mommacat/storage.rb, line 576
def loadObjects(delay_descriptor_load)
  # Load up MU::Cloud objects for all our kittens in this deploy

  MU::Cloud.resource_types.each_pair { |res_type, attrs|
    type = attrs[:cfg_plural]
    next if !@deployment.has_key?(type)

    deletia = {}
    @deployment[type].each_pair { |res_name, data|
      orig_cfg = findResourceConfig(type, res_name)

      if orig_cfg.nil?
        MU.log "Failed to locate original config for #{attrs[:cfg_name]} #{res_name} in #{@deploy_id}", MU::WARN if !["firewall_rules", "databases", "storage_pools", "cache_clusters", "alarms"].include?(type) # XXX shaddap
        next
      end

      if orig_cfg['vpc']
        ref = if orig_cfg['vpc']['id'] and orig_cfg['vpc']['id'].is_a?(Hash)
          orig_cfg['vpc']['id']['mommacat'] = self
          MU::Config::Ref.get(orig_cfg['vpc']['id'])
        else
          orig_cfg['vpc']['mommacat'] = self
          MU::Config::Ref.get(orig_cfg['vpc'])
        end
        orig_cfg['vpc'].delete('mommacat')
        orig_cfg['vpc'] = ref if ref.kitten(shallow: true)
      end

      begin
        if attrs[:has_multiples]
          data.keys.each { |mu_name|
            addKitten(type, res_name, attrs[:interface].new(mommacat: self, kitten_cfg: orig_cfg, mu_name: mu_name, delay_descriptor_load: delay_descriptor_load))
          }
        else
          addKitten(type, res_name, attrs[:interface].new(mommacat: self, kitten_cfg: orig_cfg, mu_name: data['mu_name'], cloud_id: data['cloud_id']))
        end
      rescue StandardError => e
        if e.class != MU::Cloud::MuCloudResourceNotImplemented
          MU.log "Failed to load an existing resource of type '#{type}' in #{@deploy_id}: #{e.inspect}", MU::WARN, details: e.backtrace
        end
      end
    }

  }
end
resolve_habitat(habitat, credentials: nil, debug: false) click to toggle source
# File modules/mu/mommacat/search.rb, line 236
def resolve_habitat(habitat, credentials: nil, debug: false)
  return nil if habitat.nil?
  if habitat.is_a?(MU::Config::Ref) and habitat.id
    return habitat.id
  else
    realhabitat = findLitterMate(type: "habitat", name: habitat, credentials: credentials)
    if realhabitat and realhabitat.mu_name
      return realhabitat.cloud_id
    elsif debug
      MU.log "Failed to resolve habitat name #{habitat}", MU::WARN
    end
  end
end
setDeploySecret() click to toggle source

Helper for initialize

# File modules/mu/mommacat/storage.rb, line 571
def setDeploySecret
  MU.log "Creating deploy secret for #{MU.deploy_id}"
  @deploy_secret = Password.random(256)
end
setThreadContextToMe() click to toggle source
# File modules/mu/mommacat.rb, line 915
def setThreadContextToMe

  ["appname", "environment", "timestamp", "seed", "handle"].each { |var|
    @deployment[var] ||= instance_variable_get("@#{var}".to_sym)
    if @deployment[var]
      if var != "handle"
        MU.setVar(var, @deployment[var].upcase)
      else
        MU.setVar(var, @deployment[var])
      end
    else
      MU.log "Missing global variable #{var} for #{MU.deploy_id}", MU::ERR
    end
  }
end
writeFile(filename, contents) click to toggle source
# File modules/mu/mommacat/storage.rb, line 564
def writeFile(filename, contents)
  file = File.new("#{deploy_dir}/#{filename}", File::CREAT|File::TRUNC|File::RDWR, 0600)
  file.puts contents
  file.close
end