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
Public Class Methods
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
List the currently held flock() locks.
# File modules/mu/mommacat/storage.rb, line 85 def self.locks; @lock_semaphore.synchronize { @locks } end
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
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
@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
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
(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
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 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
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 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
List the currently held flock() locks.
# File modules/mu/mommacat/storage.rb, line 81 def self.trapSafeLocks; @locks end
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
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
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
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
# 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
# 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
# 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
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
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
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
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
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
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
@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
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
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
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
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
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
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
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
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
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
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
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
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
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
@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
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
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
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
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
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
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
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
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
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
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
# 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
# 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
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
# 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
# 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
# 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
# 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
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
# 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
# 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