# Copyright:: Copyright (c) 2020 eGlobalTech, Inc., all rights reserved # # Licensed under the BSD-3 license (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License in the root of the project or at # # http://egt-labs.com/mu/LICENSE.html # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. module MU # 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*. class MommaCat # Lookup table to translate the word "habitat" back to its # provider-specific jargon HABITAT_SYNONYMS = { "AWS" => "account", "CloudFormation" => "account", "Google" => "project", "Azure" => "subscription", "VMWare" => "sddc" } # Given a cloud provider's native descriptor for a resource, make some # reasonable guesses about what the thing's name should be. 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 # 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] 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 # 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. 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 @unique_map_semaphore = Mutex.new @name_unique_str_map = {} # Keep a map of the uniqueness strings we assign to various full names, in # case we want to reuse them later. # @return [Hash] def self.name_unique_str_map @name_unique_str_map end # Keep a map of the uniqueness strings we assign to various full names, in # case we want to reuse them later. # @return [Mutex] def self.unique_map_semaphore @unique_map_semaphore 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 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 # 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] 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 # List the name/value pairs for our mandatory standard set of resource tags # for this deploy. # @return [Hash] def listStandardTags { "MU-ID" => @deploy_id, "MU-APP" => @appname, "MU-ENV" => @environment, "MU-MASTER-IP" => MU.mu_public_ip } 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] def self.listOptionalTags return { "MU-HANDLE" => MU.handle, "MU-MASTER-NAME" => Socket.gethostname, "MU-OWNER" => MU.mu_user } 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. 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 # 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 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 private # 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. 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 # 2019-06-03 adding things from https://aiweirdness.com/post/185339301987/once-again-a-neural-net-tries-to-name-cats @catadjs = %w{fuzzy ginger lilac chocolate xanthic wiggly itty chonky norty slonky floofy heckin bebby} @catnouns = %w{bastet biscuits bobcat catnip cheetah chonk dot felix hamb hambina jaguar kitty leopard lion lynx maru mittens moggy neko nip ocelot panther patches paws phoebe purr queen roar saber sekhmet skogkatt socks sphinx spot tail tiger tom whiskers wildcat yowl floof beans ailurophile dander dewclaw grimalkin kibble quick tuft misty simba slonk mew quat eek ziggy whiskeridoo cromch monch screm} @catmixed = %w{abyssinian angora bengal birman bobtail bombay burmese calico chartreux cheshire cornish-rex curl devon egyptian-mau feline furever fumbs havana himilayan japanese-bobtail javanese khao-manee maine-coon manx marmalade mau munchkin norwegian pallas persian peterbald polydactyl ragdoll russian-blue savannah scottish-fold serengeti shorthair siamese siberian singapura snowshoe stray tabby tonkinese tortoiseshell turkish-van tuxedo uncia caterwaul lilac-point chocolate-point mackerel maltese knead whitenose vorpal chewie-bean chicken-whiskey fish-especially thelonious-monsieur tom-glitter serendipitous-kill sparky-buttons nip-nops murder-mittens bite} @catwords = @catadjs + @catnouns + @catmixed @jaegeradjs = %w{azure fearless lucky olive vivid electric grey yarely violet ivory jade cinnamon crimson tacit umber mammoth ultra iron zodiac} @jaegernouns = %w{horizon hulk ultimatum yardarm watchman whilrwind wright rhythm ocean enigma eruption typhoon jaeger brawler blaze vandal excalibur paladin juliet kaleidoscope romeo} @jaegermixed = %w{alpha ajax amber avenger brave bravo charlie chocolate chrome corinthian dancer danger dash delta duet echo edge elite eureka foxtrot guardian gold hyperion illusion imperative india intercept kilo lancer night nova november oscar omega pacer quickstrike rogue ronin striker tango titan valor victor vulcan warder xenomorph xenon xray xylem yankee yell yukon zeal zero zoner zodiac} @jaegerwords = @jaegeradjs + @jaegernouns + @jaegermixed @words = @catwords + @jaegerwords end #class end #module