# # 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. require 'rubygems' require 'bundler/setup' require 'yaml' require 'socket' require 'net/http' gem 'aws-sdk-core' autoload :Aws, "aws-sdk-core" gem 'nokogiri' autoload :Nokogiri, "nokogiri" gem 'simple-password-gen' autoload :Password, "simple-password-gen" autoload :Resolv, 'resolv' gem 'netaddr' autoload :NetAddr, 'netaddr' # weird magic (possibly unnecessary) class Object # weird magic (possibly unnecessary) def metaclass class << self; self; end end end # Mu extensions to Ruby's {Hash} type for internal Mu use class Hash # Strip extraneous fields out of a {MU::Config} hash to make it suitable for # shorthand printing, such as with mu-adopt --diff def self.bok_minimize(o) if o.is_a?(Hash) newhash = o.reject { |k, v| !v.is_a?(Array) and !v.is_a?(Hash) and !["name", "id", "cloud_id"].include?(k) } # newhash.delete("cloud_id") if newhash["name"] or newhash["id"] newhash.each_pair { |k, v| newhash[k] = bok_minimize(v) } newhash.reject! { |_k, v| v.nil? or v.empty? } newhash = newhash.values.first if newhash.size == 1 return newhash elsif o.is_a?(Array) newarray = [] o.each { |v| newvalue = bok_minimize(v) newarray << newvalue if !newvalue.nil? and !newvalue.empty? } newarray = newarray.first if newarray.size == 1 return newarray end o end # A comparison function for sorting arrays of hashes def <=>(other) return 1 if other.nil? or self.size > other.size return -1 if other.size > self.size # Sort any array children we have self.each_pair { |k, v| self[k] = v.sort if v.is_a?(Array) } other.each_pair { |k, v| other[k] = v.sort if v.is_a?(Array) } return 0 if self == other # that was easy! # compare elements and decide who's "bigger" based on their totals? 0 end # Recursively compare two hashes def diff(with, on = self, level: 0, parents: []) return if with.nil? and on.nil? if with.nil? or on.nil? or with.class != on.class return # XXX ...however we're flagging differences end return if on == with tree = "" indentsize = 0 parents.each { |p| tree += (" " * indentsize) + p + " => \n" indentsize += 2 } indent = (" " * indentsize) changes = [] if on.is_a?(Hash) on_unique = (on.keys - with.keys) with_unique = (with.keys - on.keys) shared = (with.keys & on.keys) shared.each { |k| diff(with[k], on[k], level: level+1, parents: parents + [k]) } on_unique.each { |k| changes << "- ".red+PP.pp({k => on[k] }, '') } with_unique.each { |k| changes << "+ ".green+PP.pp({k => with[k]}, '') } elsif on.is_a?(Array) return if with == on # special case- Basket of Kittens lists of declared resources of a type; # we use this to decide if we can compare two array elements as if they # should be equivalent # We also implement comparison operators for {Hash} and our various # custom objects which we might find in here so that we can get away with # sorting arrays full of weird, non-primitive types. done = [] # before_a = on.dup # after_a = on.dup.sort # before_b = with.dup # after_b = with.dup.sort on.sort.each { |elt| if elt.is_a?(Hash) and elt['name'] or elt['entity']# or elt['cloud_id'] with.sort.each { |other_elt| if (elt['name'] and other_elt['name'] == elt['name']) or (elt['name'].nil? and !elt["id"].nil? and elt["id"] == other_elt["id"]) or (elt['name'].nil? and elt["id"].nil? and !elt["entity"].nil? and !other_elt["entity"].nil? and ( (elt["entity"]["id"] and elt["entity"]["id"] == other_elt["entity"]["id"]) or (elt["entity"]["name"] and elt["entity"]["name"] == other_elt["entity"]["name"]) ) ) break if elt == other_elt done << elt done << other_elt namestr = if elt['type'] "#{elt['type']}[#{elt['name']}]" elsif elt['name'] elt['name'] elsif elt['entity'] and elt["entity"]["id"] elt['entity']['id'] end diff(other_elt, elt, level: level+1, parents: parents + [namestr]) break end } end } on_unique = (on - with) - done with_unique = (with - on) - done # if on_unique.size > 0 or with_unique.size > 0 # if before_a != after_a # MU.log "A BEFORE", MU::NOTICE, details: before_a # MU.log "A AFTER", MU::NOTICE, details: after_a # end # if before_b != after_b # MU.log "B BEFORE", MU::NOTICE, details: before_b # MU.log "B AFTER", MU::NOTICE, details: after_b # end # end on_unique.each { |e| changes << if e.is_a?(Hash) "- ".red+PP.pp(Hash.bok_minimize(e), '').gsub(/\n/, "\n "+(indent)) else "- ".red+e.to_s end } with_unique.each { |e| changes << if e.is_a?(Hash) "+ ".green+PP.pp(Hash.bok_minimize(e), '').gsub(/\n/, "\n "+(indent)) else "+ ".green+e.to_s end } else if on != with changes << "-".red+" #{on.to_s}" changes << "+".green+" #{with.to_s}" end end if changes.size > 0 puts tree changes.each { |c| puts indent+c } end end # Implement a merge! that just updates each hash leaf as needed, not # trashing the branch on the way there. def deep_merge!(with, on = self) if on and with and with.is_a?(Hash) with.each_pair { |k, v| if !on[k] or !on[k].is_a?(Hash) on[k] = v else deep_merge!(with[k], on[k]) end } elsif with on = with end on end end ENV['HOME'] = Etc.getpwuid(Process.uid).dir require 'mu/logger' module MU # Subclass core thread so we can gracefully handle it when we hit system # thread limits. Back off and wait makes sense for us, since most of our # threads are terminal (in the dependency sense) and this is unlikely to get # us deadlocks. class Thread < ::Thread @@mu_global_threads = [] @@mu_global_thread_semaphore = Mutex.new def initialize(*args, &block) @@mu_global_thread_semaphore.synchronize { @@mu_global_threads.reject! { |t| t.nil? or !t.status } } newguy = nil start = Time.now begin newguy = super(*args, &block) if newguy.nil? MU.log "I somehow got a nil trying to create a thread", MU::WARN, details: caller sleep 1 end rescue ::ThreadError => e if e.message.match(/Resource temporarily unavailable/) toomany = @@mu_global_threads.size MU.log "Hit the wall at #{toomany.to_s} threads, waiting until there are fewer", MU::WARN if @@mu_global_threads.size >= toomany sleep 1 begin @@mu_global_thread_semaphore.synchronize { @@mu_global_threads.each { |t| next if t == ::Thread.current t.join(0.1) } @@mu_global_threads.reject! { |t| t.nil? or !t.status } } if (Time.now - start) > 150 MU.log "Failed to get a free thread slot after 150 seconds- are we in a deadlock situation?", MU::ERR, details: caller raise e end end while @@mu_global_threads.size >= toomany end retry else raise e end end while newguy.nil? @@mu_global_thread_semaphore.synchronize { @@mu_global_threads << newguy } end end # Wrapper class for fatal Exceptions. Gives our internals something to # inherit that will log an error message appropriately before bubbling up. class MuError < StandardError def initialize(message = nil) MU.log message, MU::ERR, details: caller[2] if !message.nil? if MU.verbosity == MU::Logger::SILENT super "" else super message end end end # Wrapper class for temporary Exceptions. Gives our internals something to # inherit that will log a notice message appropriately before bubbling up. class MuNonFatal < StandardError def initialize(message = nil) MU.log message, MU::NOTICE if !message.nil? if MU.verbosity == MU::Logger::SILENT super "" else super message end end end if !ENV.has_key?("MU_LIBDIR") and ENV.has_key?("MU_INSTALLDIR") ENV['MU_LIBDIR'] = ENV['MU_INSTALLDIR']+"/lib" else ENV['MU_LIBDIR'] = File.realpath(File.expand_path(File.dirname(__FILE__))+"/../") end # Mu's installation directory. @@myRoot = File.expand_path(ENV['MU_LIBDIR']) # Mu's installation directory. # @return [String] def self.myRoot; @@myRoot end # utility routine for sorting semantic versioning strings def self.version_sort(a, b) a_parts = a.split(/[^a-z0-9]/) b_parts = b.split(/[^a-z0-9]/) for i in 0..a_parts.size matchval = if a_parts[i] and b_parts[i] and a_parts[i].match(/^\d+/) and b_parts[i].match(/^\d+/) a_parts[i].to_i <=> b_parts[i].to_i elsif a_parts[i] and !b_parts[i] 1 elsif !a_parts[i] and b_parts[i] -1 else a_parts[i] <=> b_parts[i] end return matchval if matchval != 0 end 0 end # Front our global $MU_CFG hash with a read-only copy def self.muCfg Marshal.load(Marshal.dump($MU_CFG)).freeze end # Returns true if we're running without a full systemwide Mu Master install, # typically as a gem. def self.localOnly ((Gem.paths and Gem.paths.home and File.realpath(File.expand_path(File.dirname(__FILE__))).match(/^#{Gem.paths.home}/)) or !Dir.exist?("/opt/mu")) end # Are we operating in a gem? def self.inGem? return @in_gem if defined? @in_gem if Gem.paths and Gem.paths.home and File.dirname(__FILE__).match(/^#{Gem.paths.home}/) @in_gem = true else @in_gem = false end end # The main (root) Mu user's data directory. @@mainDataDir = File.expand_path(@@myRoot+"/../var") # The main (root) Mu user's data directory. # @return [String] def self.mainDataDir; @@mainDataDir end # The Mu config directory @@etcDir = File.expand_path(@@myRoot+"/../etc") # The Mu config directory # @return [String] def self.etcDir; @@etcDir end # The Mu install directory @@installDir = File.expand_path(@@myRoot+"/..") # The Mu install directory # @return [String] def self.installDir; @@installDir end # Mu's main metadata directory (also the deployment metadata for the 'mu' @@globals = Hash.new @@globals[Thread.current.object_id] = Hash.new # Rig us up to share some global class variables (as MU.var_name). # These values are PER-THREAD, so that things like Momma Cat can be more or # less thread-safe with global values. def self.globals; @@globals end @@global_var_semaphore = Mutex.new # Set one of our global per-thread variables. def self.setVar(name, value) @@global_var_semaphore.synchronize { @@globals[Thread.current.object_id] ||= Hash.new @@globals[Thread.current.object_id][name] ||= Hash.new @@globals[Thread.current.object_id][name] = value } end # Copy the set of global variables in use by another thread, typically our # parent thread. def self.dupGlobals(parent_thread_id) @@globals[parent_thread_id] ||= {} @@globals[parent_thread_id].each_pair { |name, value| setVar(name, value) } end # Expunge all global variables. def self.purgeGlobals @@globals.delete(Thread.current.object_id) end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.mommacat; @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['mommacat'] end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.deploy_id; @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['deploy_id'] end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.appname; @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['appname'] end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.environment; @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['environment'] end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.timestamp; @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['timestamp'] end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.seed; @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['seed'] end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.handle; @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['handle'] end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.chef_user; @@globals[Thread.current.object_id] ||= {} if @@globals.has_key?(Thread.current.object_id) and @@globals[Thread.current.object_id].has_key?('chef_user') @@globals[Thread.current.object_id]['chef_user'] elsif Etc.getpwuid(Process.uid).name == "root" return "mu" else return Etc.getpwuid(Process.uid).name end end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.mu_user @@globals[Thread.current.object_id] ||= {} if @@globals.has_key?(Thread.current.object_id) and @@globals[Thread.current.object_id].has_key?('mu_user') return @@globals[Thread.current.object_id]['mu_user'] elsif Etc.getpwuid(Process.uid).name == "root" return "mu" else return Etc.getpwuid(Process.uid).name end end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.curRegion @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['curRegion'] ||= myRegion || ENV['EC2_REGION'] end # Accessor for per-thread global variable. There is probably a Ruby-clever way to define this. def self.syncLitterThread; @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['syncLitterThread'] end # Mu's deployment metadata directory. @myDataDir = File.expand_path(ENV['MU_DATADIR']) if ENV.has_key?("MU_DATADIR") @myDataDir = @@mainDataDir if @myDataDir.nil? # Mu's deployment metadata directory. def self.dataDir(for_user = MU.mu_user) if !localOnly and ((Process.uid == 0 and (for_user.nil? or for_user.empty?)) or for_user == "mu" or for_user == "root") return @myDataDir else for_user ||= MU.mu_user basepath = Etc.getpwnam(for_user).dir+"/.mu" Dir.mkdir(basepath, 0755) if !Dir.exist?(basepath) Dir.mkdir(basepath+"/var", 0755) if !Dir.exist?(basepath+"/var") return basepath+"/var" end end # Return the verbosity setting of the default @@logger object def self.verbosity @@logger ? @@logger.verbosity : MU::Logger::NORMAL end # Set parameters parameters for calls to {MU#log} def self.setLogging(verbosity, webify_logs = false, handle = STDOUT, color = true) @@logger ||= MU::Logger.new(verbosity, webify_logs, handle, color) @@logger.html = webify_logs @@logger.verbosity = verbosity @@logger.handle = handle @@logger.color = color end setLogging(MU::Logger::NORMAL, false) # Shortcut to get SUMMARY messages from the global MU::Logger instance # @return [Array] def self.summary @@logger.summary end # Shortcut to invoke {MU::Logger#log} def self.log(msg, level = MU::INFO, details: nil, html: false, verbosity: nil, color: true) return if (level == MU::DEBUG and verbosity and verbosity <= MU::Logger::LOUD) return if verbosity and verbosity == MU::Logger::SILENT if (level == MU::ERR or level == MU::WARN or level == MU::DEBUG or (verbosity and verbosity >= MU::Logger::LOUD) or (level == MU::NOTICE and !details.nil?)) and Thread.current.thread_variable_get("name") newdetails = { :thread => Thread.current.object_id, :name => Thread.current.thread_variable_get("name") } newdetails[:details] = details.dup if details details = newdetails end @@logger.log(msg, level, details: details, html: html, verbosity: verbosity, color: color) end # For log entries that should only be logged when we're in verbose mode DEBUG = 0.freeze # For ordinary log entries INFO = 1.freeze # For more interesting log entries which are not errors NOTICE = 2.freeze # Log entries for non-fatal errors WARN = 3.freeze # Log entries for non-fatal errors WARNING = 3.freeze # Log entries for fatal errors ERR = 4.freeze # Log entries for fatal errors ERROR = 4.freeze # Log entries that will be held and displayed/emailed at the end of deploy, # cleanup, etc. SUMMARY = 5.freeze autoload :Cleanup, 'mu/cleanup' autoload :Deploy, 'mu/deploy' autoload :MommaCat, 'mu/mommacat' autoload :Master, 'mu/master' require 'mu/cloud' require 'mu/groomer' # Little hack to initialize library-only environments' config files def self.detectCloudProviders MU.log "Auto-detecting cloud providers" new_cfg = $MU_CFG.dup examples = {} MU::Cloud.supportedClouds.each { |cloud| cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud) begin if cloudclass.hosted? and !$MU_CFG[cloud.downcase] cfg_blob = cloudclass.hosted_config if cfg_blob new_cfg[cloud.downcase] = cfg_blob MU.log "Adding auto-detected #{cloud} stanza", MU::NOTICE end elsif !$MU_CFG[cloud.downcase] and !cloudclass.config_example.nil? examples[cloud.downcase] = cloudclass.config_example end rescue NoMethodError => e # missing .hosted? is normal for dummy layers like CloudFormation MU.log e.message, MU::WARN end } new_cfg['auto_detection_done'] = true if new_cfg != $MU_CFG or !cfgExists? MU.log "Generating #{cfgPath}" saveMuConfig(new_cfg, examples) # XXX and reload it end new_cfg end if !$MU_CFG require "#{@@myRoot}/bin/mu-load-config.rb" if !$MU_CFG['auto_detection_done'] and (!$MU_CFG['multiuser'] or !cfgExists?) detectCloudProviders end end @@mommacat_port = 2260 if !$MU_CFG.nil? and !$MU_CFG['mommacat_port'].nil? and !$MU_CFG['mommacat_port'] != "" and $MU_CFG['mommacat_port'].to_i > 0 and $MU_CFG['mommacat_port'].to_i < 65536 @@mommacat_port = $MU_CFG['mommacat_port'].to_i end # The port on which the Momma Cat daemon should listen for requests # @return [Integer] def self.mommaCatPort @@mommacat_port end @@my_private_ip = nil @@my_public_ip = nil @@mu_public_addr = nil @@mu_public_ip = nil if MU::Cloud::AWS.hosted? @@my_private_ip = MU::Cloud::AWS.getAWSMetaData("local-ipv4") @@my_public_ip = MU::Cloud::AWS.getAWSMetaData("public-ipv4") @@mu_public_addr = @@my_public_ip @@mu_public_ip = @@my_public_ip end if !$MU_CFG.nil? and !$MU_CFG['public_address'].nil? and !$MU_CFG['public_address'].empty? and @@my_public_ip != $MU_CFG['public_address'] @@mu_public_addr = $MU_CFG['public_address'] if !@@mu_public_addr.match(/^\d+\.\d+\.\d+\.\d+$/) hostname = IO.readlines("/etc/hostname")[0].gsub /\n/, '' hostlines = File.open('/etc/hosts').grep(/.*#{hostname}.*/) if hostlines and !hostlines.empty? @@mu_public_ip = hostlines.first.match(/^\d+\.\d+\.\d+\.\d+/)[0] end else @@mu_public_ip = @@mu_public_addr end elsif !@@my_public_ip.nil? and !@@my_public_ip.empty? @@mu_public_addr = @@my_public_ip @@mu_public_ip = @@my_public_ip else @@mu_public_addr = @@my_private_ip @@mu_public_ip = @@my_private_ip end # This machine's private IP address def self.my_private_ip; @@my_private_ip end # This machine's public IP address def self.my_public_ip; @@my_public_ip end # Public Mu server name, not necessarily the same as MU.my_public_ip (an be a proxy, load balancer, etc) def self.mu_public_ip; @@mu_public_ip end # Public Mu server IP address, not necessarily the same as MU.my_public_ip (an be a proxy, load balancer, etc) def self.mu_public_addr; @@mu_public_addr end mu_user = Etc.getpwuid(Process.uid).name chef_user = Etc.getpwuid(Process.uid).name.gsub(/\./, "") chef_user = "mu" if chef_user == "root" MU.setVar("chef_user", chef_user) MU.setVar("mu_user", mu_user) @userlist = nil # Fetch the email address of a given Mu user def self.userEmail(user = MU.mu_user) @userlist ||= MU::Master.listUsers user = "mu" if user == "root" if Dir.exist?("#{MU.mainDataDir}/users/#{user}") and File.readable?("#{MU.mainDataDir}/users/#{user}/email") and File.size?("#{MU.mainDataDir}/users/#{user}/email") return File.read("#{MU.mainDataDir}/users/#{user}/email").chomp elsif @userlist.has_key?(user) return @userlist[user]['email'] else MU.log "Attempted to load nonexistent user #{user}", MU::ERR return nil end end # Fetch the real-world name of a given Mu user def self.userName(user = MU.mu_user) @userlist ||= MU::Master.listUsers if Dir.exist?("#{MU.mainDataDir}/users/#{user}") and File.readable?("#{MU.mainDataDir}/users/#{user}/realname") and File.size?("#{MU.mainDataDir}/users/#{user}/realname") return File.read("#{MU.mainDataDir}/users/#{user}/realname").chomp elsif @userlist.has_key?(user) return @userlist[user]['email'] else MU.log "Attempted to load nonexistent user #{user}", MU::ERR return nil end end # XXX these guys to move into mu/groomer # List of known/supported grooming agents (configuration management tools) def self.supportedGroomers ["Chef", "Ansible"] end # The version of Chef we will install on nodes. @@chefVersion = "14.0.190" # The version of Chef we will install on nodes. # @return [String] def self.chefVersion @@chefVersion end MU.supportedGroomers.each { |groomer| require "mu/groomers/#{groomer.downcase}" } # @param groomer [String]: The grooming agent to load. # @return [Class]: The class object implementing this groomer agent def self.loadGroomer(groomer) if !File.size?(MU.myRoot+"/modules/mu/groomers/#{groomer.downcase}.rb") raise MuError, "Requested to use unsupported grooming agent #{groomer}" end require "mu/groomers/#{groomer.downcase}" return Object.const_get("MU").const_get("Groomer").const_get(groomer) end @@myRegion_var = nil # Find the cloud provider region where this master resides, if any def self.myRegion if MU::Cloud::Google.hosted? zone = MU::Cloud::Google.getGoogleMetaData("instance/zone") @@myRegion_var = zone.gsub(/^.*?\/|\-\d+$/, "") elsif MU::Cloud::AWS.hosted? @@myRegion_var ||= MU::Cloud::AWS.myRegion elsif MU::Cloud::Azure.hosted? @@myRegion_var ||= MU::Cloud::Azure.myRegion else @@myRegion_var = nil end @@myRegion_var end require 'mu/config' require 'mu/adoption' # Figure out what cloud provider we're in, if any. # @return [String]: Google, AWS, etc. Returns nil if we don't seem to be in a cloud. def self.myCloud if MU::Cloud::Google.hosted? @@myInstanceId = MU::Cloud::Google.getGoogleMetaData("instance/name") return "Google" elsif MU::Cloud::AWS.hosted? @@myInstanceId = MU::Cloud::AWS.getAWSMetaData("instance-id") return "AWS" elsif MU::Cloud::Azure.hosted? metadata = MU::Cloud::Azure.get_metadata()["compute"] @@myInstanceId = MU::Cloud::Azure::Id.new("/subscriptions/"+metadata["subscriptionId"]+"/resourceGroups/"+metadata["resourceGroupName"]+"/providers/Microsoft.Compute/virtualMachines/"+metadata["name"]) return "Azure" end nil end # Wrapper for {MU::Cloud::AWS.account_number} def self.account_number if !@@globals[Thread.current.object_id].nil? and !@@globals[Thread.current.object_id]['account_number'].nil? return @@globals[Thread.current.object_id]['account_number'] end @@globals[Thread.current.object_id] ||= {} @@globals[Thread.current.object_id]['account_number'] = MU::Cloud::AWS.account_number @@globals[Thread.current.object_id]['account_number'] end # The cloud instance identifier of this Mu master def self.myInstanceId return nil if MU.myCloud.nil? @@myInstanceId # MU.myCloud will have set this, since it's our test variable end # If our Mu master is hosted in a cloud provider, we can use this to get its # cloud API descriptor. def self.myCloudDescriptor; @@myCloudDescriptor end @@myAZ_var = nil # Find the cloud provider availability zone where this master resides, if any def self.myAZ if MU::Cloud::Google.hosted? zone = MU::Cloud::Google.getGoogleMetaData("instance/zone") @@myAZ_var = zone.gsub(/.*?\//, "") elsif MU::Cloud::AWS.hosted? return nil if MU.myCloudDescriptor.nil? begin @@myAZ_var ||= MU.myCloudDescriptor.placement.availability_zone rescue Aws::EC2::Errors::InternalError => e MU.log "Got #{e.inspect} on MU::Cloud::AWS.ec2(region: #{MU.myRegion}).describe_instances(instance_ids: [#{@@myInstanceId}])", MU::WARN sleep 10 end end @@myAZ_var end # Recursively turn a Ruby OpenStruct into a Hash # @param struct [OpenStruct] # @param stringify_keys [Boolean] # @return [Hash] def self.structToHash(struct, stringify_keys: false) google_struct = false begin google_struct = struct.class.ancestors.include?(::Google::Apis::Core::Hashable) rescue NameError end aws_struct = false begin aws_struct = struct.class.ancestors.include?(::Seahorse::Client::Response) rescue NameError end azure_struct = false begin azure_struct = struct.class.ancestors.include?(::MsRestAzure) or struct.class.name.match(/Azure::.*?::Mgmt::.*?::Models::/) rescue NameError end if struct.is_a?(Struct) or struct.class.ancestors.include?(Struct) or google_struct or aws_struct or azure_struct hash = if azure_struct MU::Cloud::Azure.respToHash(struct) else struct.to_h end if stringify_keys newhash = {} hash.each_pair { |k, v| newhash[k.to_s] = v } hash = newhash end hash.each_pair { |key, value| hash[key] = self.structToHash(value, stringify_keys: stringify_keys) } return hash elsif struct.is_a?(MU::Config::Ref) struct = struct.to_h elsif struct.is_a?(MU::Cloud::Azure::Id) struct = struct.to_s elsif struct.is_a?(Hash) if stringify_keys newhash = {} struct.each_pair { |k, v| newhash[k.to_s] = v } struct = newhash end struct.each_pair { |key, value| struct[key] = self.structToHash(value, stringify_keys: stringify_keys) } return struct elsif struct.is_a?(Array) struct.map! { |elt| self.structToHash(elt, stringify_keys: stringify_keys) } elsif struct.is_a?(String) # Cleanse weird encoding problems return struct.dup.to_s.force_encoding("ASCII-8BIT").encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') else return struct end end @@myCloudDescriptor = nil if MU.myCloud svrclass = const_get("MU").const_get("Cloud").const_get(MU.myCloud).const_get("Server") found = svrclass.find(cloud_id: @@myInstanceId, region: MU.myRegion) # XXX need habitat arg for google et al # found = MU::MommaCat.findStray(MU.myCloud, "server", cloud_id: @@myInstanceId, dummy_ok: true, region: MU.myRegion) if !found.nil? and found.size == 1 @@myCloudDescriptor = found.values.first end end @@myVPCObj_var = nil # The VPC/Network in which this Mu master resides def self.myVPCObj return nil if MU.myCloud.nil? return @@myVPCObj_var if @@myVPCObj_var cloudclass = const_get("MU").const_get("Cloud").const_get(MU.myCloud) @@myVPCObj_var ||= cloudclass.myVPCObj @@myVPCObj_var end @@myVPC_var = nil # The VPC/Network in which this Mu master resides def self.myVPC return nil if MU.myCloud.nil? return @@myVPC_var if @@myVPC_var my_vpc_desc = MU.myVPCObj @@myVPC_var ||= my_vpc_desc.cloud_id if my_vpc_desc @@myVPC_var end # Mu's SSL certificate directory @@mySSLDir = MU.dataDir+"/ssl" if MU.dataDir @@mySSLDir ||= File.realpath(File.expand_path(File.dirname(__FILE__))+"/../var/ssl") # Mu's SSL certificate directory # @return [String] def self.mySSLDir @@mySSLDir end # Recursively compare two hashes. Intended to see when cloud API descriptions # of existing resources differ from proposed changes so we know when to # bother updating. # @param hash1 [Hash]: The first hash # @param hash2 [Hash]: The second hash # @param missing_is_default [Boolean]: Assume that any element missing from hash2 but present in hash1 is a default value to be ignored # @return [Boolean] def self.hashCmp(hash1, hash2, missing_is_default: false) return false if hash1.nil? hash2.keys.each { |k| if hash1[k].nil? return false end } if !missing_is_default hash1.keys.each { |k| if hash2[k].nil? return false end } end hash1.keys.each { |k| if hash1[k].is_a?(Array) return false if !missing_is_default and hash2[k].nil? if !hash2[k].nil? hash2[k].each { |item| if !hash1[k].include?(item) return false end } end elsif hash1[k].is_a?(Hash) and !hash2[k].nil? result = hashCmp(hash1[k], hash2[k], missing_is_default: missing_is_default) return false if !result else if missing_is_default return false if !hash2[k].nil? and hash1[k] != hash2[k] else return false if hash1[k] != hash2[k] end end } true end # Given a hash, or an array that might contain a hash, change all of the keys # to symbols. Useful for formatting option parameters to some APIs. def self.strToSym(obj) if obj.is_a?(Hash) newhash = {} obj.each_pair { |k, v| if v.is_a?(Hash) or v.is_a?(Array) newhash[k.to_sym] = MU.strToSym(v) else newhash[k.to_sym] = v end } newhash elsif obj.is_a?(Array) newarr = [] obj.each { |v| if v.is_a?(Hash) or v.is_a?(Array) newarr << MU.strToSym(v) else newarr << v end } newarr end end # Generate a random password which will satisfy the complexity requirements of stock Amazon Windows AMIs. # return [String]: A password string. def self.generateWindowsPassword(safe_pattern: '~!@#%^&*_-+=`|(){}[]:;<>,.?', retries: 25) # We have dopey complexity requirements, be stringent here. # I'll be nice and not condense this into one elegant-but-unreadable regular expression attempts = 0 safe_metachars = Regexp.escape(safe_pattern) begin if attempts > retries MU.log "Failed to generate an adequate Windows password after #{attempts}", MU::ERR raise MuError, "Failed to generate an adequate Windows password after #{attempts}" end winpass = Password.random(14..16) attempts += 1 end while winpass.nil? or !winpass.match(/^[a-z]/i) or !winpass.match(/[A-Z]/) or !winpass.match(/[a-z]/) or !winpass.match(/\d/) or !winpass.match(/[#{safe_metachars}]/) or winpass.match(/[^\w\d#{safe_metachars}]/) MU.log "Generated Windows password after #{attempts} attempts", MU::DEBUG return winpass end # Return the name of the Mu log and key bucket for this Mu server. Not # necessarily in any specific cloud provider. # @return [String] def self.adminBucketName(platform = nil, credentials: nil) return nil if platform and !MU::Cloud.supportedClouds.include?(platform) clouds = platform.nil? ? MU::Cloud.supportedClouds : [platform] clouds.each { |cloud| cloudclass = Object.const_get("MU").const_get("Cloud").const_get(cloud) bucketname = cloudclass.adminBucketName(credentials) begin if platform or (cloudclass.hosted? and platform.nil?) or cloud == MU::Config.defaultCloud return bucketname end end } return bucketname end end