# 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 # Plugins under this namespace serve as interfaces to cloud providers and # other provisioning layers. class Cloud # Generic class methods (.find, .cleanup, etc) are defined in wrappers.rb require 'mu/cloud/wrappers' @@resource_types.each_key { |name| Object.const_get("MU").const_get("Cloud").const_get(name).class_eval { attr_reader :cloudclass attr_reader :cloudobj attr_reader :destroyed attr_reader :delayed_save # Print something palatable when we're called in a string context. def to_s fullname = "#{self.class.shortname}" if !@cloudobj.nil? and !@cloudobj.mu_name.nil? @mu_name ||= @cloudobj.mu_name end if !@mu_name.nil? and !@mu_name.empty? fullname = fullname + " '#{@mu_name}'" end if !@cloud_id.nil? fullname = fullname + " (#{@cloud_id})" end return fullname end # Set our +deploy+ and +deploy_id+ attributes, optionally doing so even # if they have already been set. # # @param mommacat [MU::MommaCat]: The deploy to which we're being told we belong # @param force [Boolean]: Set even if we already have a deploy object # @return [String]: Our new +deploy_id+ def intoDeploy(mommacat, force: false) if force or (!@deploy) MU.log "Inserting #{self} [#{self.object_id}] into #{mommacat.deploy_id} as a #{@config['name']}", MU::DEBUG @deploy = mommacat @deploy.addKitten(@cloudclass.cfg_plural, @config['name'], self) @deploy_id = @deploy.deploy_id @cloudobj.intoDeploy(mommacat, force: force) if @cloudobj end @deploy_id end # Return the +virtual_name+ config field, if it is set. # @param name [String]: If set, will only return a value if +virtual_name+ matches this string # @return [String,nil] def virtual_name(name = nil) if @config and @config['virtual_name'] and (!name or name == @config['virtual_name']) return @config['virtual_name'] end nil end # @param mommacat [MU::MommaCat]: The deployment containing this cloud resource # @param mu_name [String]: Optional- specify the full Mu resource name of an existing resource to load, instead of creating a new one # @param cloud_id [String]: Optional- specify the cloud provider's identifier for an existing resource to load, instead of creating a new one # @param kitten_cfg [Hash]: The parse configuration for this object from {MU::Config} def initialize(**args) raise MuError, "Cannot invoke Cloud objects without a configuration" if args[:kitten_cfg].nil? # We are a parent wrapper object. Initialize our child object and # housekeeping bits accordingly. if self.class.name =~ /^MU::Cloud::([^:]+)$/ @live = true @delayed_save = args[:delayed_save] @method_semaphore = Mutex.new @method_locks = {} if args[:mommacat] MU.log "Initializing an instance of #{self.class.name} in #{args[:mommacat].deploy_id} #{mu_name}", MU::DEBUG, details: args[:kitten_cfg] elsif args[:mu_name].nil? raise MuError, "Can't instantiate a MU::Cloud object with a live deploy or giving us a mu_name" else MU.log "Initializing a detached #{self.class.name} named #{args[:mu_name]}", MU::DEBUG, details: args[:kitten_cfg] end my_cloud = args[:kitten_cfg]['cloud'].to_s || MU::Config.defaultCloud if (my_cloud.nil? or my_cloud.empty?) and args[:mommacat] my_cloud = args[:mommacat].original_config['cloud'] end if my_cloud.nil? or !MU::Cloud.supportedClouds.include?(my_cloud) raise MuError, "Can't instantiate a MU::Cloud object without a valid cloud (saw '#{my_cloud}')" end @cloudclass = MU::Cloud.resourceClass(my_cloud, self.class.shortname) @cloud_desc_cache ||= args[:from_cloud_desc] if args[:from_cloud_desc] @cloudparentclass = MU::Cloud.cloudClass(my_cloud) @cloudobj = @cloudclass.new( mommacat: args[:mommacat], kitten_cfg: args[:kitten_cfg], cloud_id: args[:cloud_id], mu_name: args[:mu_name] ) raise MuError, "Unknown error instantiating #{self}" if @cloudobj.nil? # These should actually call the method live instead of caching a static value PUBLIC_ATTRS.each { |a| begin instance_variable_set(("@"+a.to_s).to_sym, @cloudobj.send(a)) rescue NoMethodError => e MU.log "#{@cloudclass.name} failed to implement method '#{a}'", MU::ERR, details: e.message raise e end } @deploy ||= args[:mommacat] @deploy_id ||= @deploy.deploy_id if @deploy # Register with the containing deployment if !@deploy.nil? and !@cloudobj.mu_name.nil? and !@cloudobj.mu_name.empty? and !args[:delay_descriptor_load] describe # XXX is this actually safe here? @deploy.addKitten(self.class.cfg_name, @config['name'], self) elsif !@deploy.nil? and @cloudobj.mu_name.nil? MU.log "#{self} in #{@deploy.deploy_id} didn't generate a mu_name after being loaded/initialized, dependencies on this resource will probably be confused!", MU::ERR, details: [caller, args.keys] end # We are actually a child object invoking this via super() from its # own initialize(), so initialize all the attributes and instance # variables we know to be universal. else class << self # Declare attributes that everyone should have PUBLIC_ATTRS.each { |a| attr_reader a } end # XXX this butchers ::Id and ::Ref objects that might be used by dependencies() to good effect, but we also can't expect our implementations to cope with knowing when a .to_s has to be appended to things at random @config = MU::Config.manxify(args[:kitten_cfg]) || MU::Config.manxify(args[:config]) if !@config MU.log "Missing config arguments in setInstanceVariables, can't initialize a cloud object without it", MU::ERR, details: args.keys raise MuError, "Missing config arguments in setInstanceVariables" end @deploy = args[:mommacat] || args[:deploy] @cloud_desc_cache ||= args[:from_cloud_desc] if args[:from_cloud_desc] @credentials = args[:credentials] @credentials ||= @config['credentials'] @cloud = @config['cloud'] if !@cloud if self.class.name =~ /^MU::Cloud::([^:]+)(?:::.+|$)/ cloudclass_name = Regexp.last_match[1] if MU::Cloud.supportedClouds.include?(cloudclass_name) @cloud = cloudclass_name end end end if !@cloud raise MuError, "Failed to determine what cloud #{self} should be in!" end @environment = @config['environment'] if @deploy @deploy_id = @deploy.deploy_id @appname = @deploy.appname end @cloudclass = MU::Cloud.resourceClass(@cloud, self.class.shortname) @cloudparentclass = MU::Cloud.cloudClass(@cloud) # A pre-existing object, you say? if args[:cloud_id] # TODO implement ::Id for every cloud... and they should know how to get from # cloud_desc to a fully-resolved ::Id object, not just the short string @cloud_id = args[:cloud_id] describe(cloud_id: @cloud_id) @habitat_id = habitat_id # effectively, cache this # If we can build us an ::Id object for @cloud_id instead of a # string, do so. begin idclass = @cloudparentclass.const_get(:Id) long_id = if @deploydata and @deploydata[idclass.idattr.to_s] @deploydata[idclass.idattr.to_s] elsif self.respond_to?(idclass.idattr) self.send(idclass.idattr) end @cloud_id = idclass.new(long_id) if !long_id.nil? and !long_id.empty? # 1 see if we have the value on the object directly or in deploy data # 2 set an attr_reader with the value # 3 rewrite our @cloud_id attribute with a ::Id object rescue NameError, MU::Cloud::MuCloudResourceNotImplemented end end # Use pre-existing mu_name (we're probably loading an extant deploy) # if available if args[:mu_name] @mu_name = args[:mu_name].dup # If scrub_mu_isms is set, our mu_name is always just the bare name # field of the resource. elsif @config['scrub_mu_isms'] @mu_name = @config['name'].dup # XXX feck it insert an inheritable method right here? Set a default? How should resource implementations determine whether they're instantiating a new object? end @tags = {} if !@config['scrub_mu_isms'] @tags = @deploy ? @deploy.listStandardTags : MU::MommaCat.listStandardTags end if @config['tags'] @config['tags'].each { |tag| @tags[tag['key']] = tag['value'] } end MU::MommaCat.listOptionalTags.each_pair { |k, v| @tags[k] ||= v if v } if @cloudparentclass.respond_to?(:resourceInitHook) @cloudparentclass.resourceInitHook(self, @deploy) end # Add cloud-specific instance methods for our resource objects to # inherit. if @cloudparentclass.const_defined?(:AdditionalResourceMethods) self.extend @cloudparentclass.const_get(:AdditionalResourceMethods) end if ["Server", "ServerPool"].include?(self.class.shortname) and @deploy @mu_name ||= @deploy.getResourceName(@config['name'], need_unique_string: @config.has_key?("basis")) if self.class.shortname == "Server" @groomer = MU::Groomer.new(self) end @groomclass = MU::Groomer.loadGroomer(@config["groomer"]) if windows? or @config['active_directory'] and !@mu_windows_name if !@deploydata.nil? and !@deploydata['mu_windows_name'].nil? @mu_windows_name = @deploydata['mu_windows_name'] else # Use the same random differentiator as the "real" name if we're # from a ServerPool. Helpful for admin sanity. unq = @mu_name.sub(/^.*?-(...)$/, '\1') if @config['basis'] and !unq.nil? and !unq.empty? @mu_windows_name = @deploy.getResourceName(@config['name'], max_length: 15, need_unique_string: true, use_unique_string: unq, reuse_unique_string: true) else @mu_windows_name = @deploy.getResourceName(@config['name'], max_length: 15, need_unique_string: true) end end end class << self attr_reader :groomer attr_reader :groomerclass attr_accessor :mu_windows_name # XXX might be ok as reader now end end @tags["Name"] ||= @mu_name if @mu_name end end def cloud if @cloud @cloud elsif @config and @config['cloud'] @config['cloud'] elsif self.class.name =~ /^MU::Cloud::([^:]+)::.+/ cloudclass_name = Regexp.last_match[1] if MU::Cloud.supportedClouds.include?(cloudclass_name) cloudclass_name else nil end else nil end end # Remove all metadata and cloud resources associated with this object def destroy if self.class.cfg_name == "server" begin ip = canonicalIP MU::Master.removeIPFromSSHKnownHosts(ip) if ip if @deploy and @deploy.deployment and @deploy.deployment['servers'] and @config['name'] me = @deploy.deployment['servers'][@config['name']][@mu_name] if me ["private_ip_address", "public_ip_address"].each { |field| if me[field] MU::Master.removeIPFromSSHKnownHosts(me[field]) end } if me["private_ip_list"] me["private_ip_list"].each { |private_ip| MU::Master.removeIPFromSSHKnownHosts(private_ip) } end end end rescue MU::MuError => e MU.log e.message, MU::WARN end end if !@cloudobj.nil? and !@cloudobj.groomer.nil? @cloudobj.groomer.cleanup elsif !@groomer.nil? @groomer.cleanup end if !@deploy.nil? if !@cloudobj.nil? and !@config.nil? and !@cloudobj.mu_name.nil? @deploy.notify(self.class.cfg_plural, @config['name'], nil, mu_name: @cloudobj.mu_name, remove: true, triggering_node: @cloudobj, delayed_save: @delayed_save) elsif !@mu_name.nil? @deploy.notify(self.class.cfg_plural, @config['name'], nil, mu_name: @mu_name, remove: true, triggering_node: self, delayed_save: @delayed_save) end @deploy.removeKitten(self) end # Make sure that if notify gets called again it won't go returning a # bunch of now-bogus metadata. @destroyed = true if !@cloudobj.nil? def @cloudobj.notify {} end else def notify {} end end end # Return the cloud object's idea of where it lives (project, account, # etc) in the form of an identifier. If not applicable for this object, # we expect to return +nil+. # @return [String,nil] def habitat(nolookup: true) return nil if ["folder", "habitat"].include?(self.class.cfg_name) if @cloudobj @cloudparentclass.habitat(@cloudobj, nolookup: nolookup, deploy: @deploy) else @cloudparentclass.habitat(self, nolookup: nolookup, deploy: @deploy) end end def habitat_id(nolookup: false) @habitat_id ||= habitat(nolookup: nolookup) @habitat_id end # We're fundamentally a wrapper class, so go ahead and reroute requests # that are meant for our wrapped object. def method_missing(method_sym, *arguments) if @cloudobj MU.log "INVOKING #{method_sym} FROM PARENT CLOUD OBJECT #{self}", MU::DEBUG, details: arguments @cloudobj.method(method_sym).call(*arguments) else raise NoMethodError, "No such instance method #{method_sym} available on #{self.class.name}" end end # Merge the passed hash into the existing configuration hash of this # cloud object. Currently this is only used by the {MU::Adoption} # module. I don't love exposing this to the whole internal API, but I'm # probably overthinking that. # @param newcfg [Hash] def config!(newcfg) @config.merge!(newcfg) end def cloud_desc(use_cache: true) describe if !@cloudobj.nil? if @cloudobj.class.instance_methods(false).include?(:cloud_desc) @cloud_desc_cache ||= @cloudobj.cloud_desc end end if !@config.nil? and !@cloud_id.nil? and (!use_cache or @cloud_desc_cache.nil?) # The find() method should be returning a Hash with the cloud_id # as a key and a cloud platform descriptor as the value. begin args = { :region => @config['region'], :cloud => @config['cloud'], :cloud_id => @cloud_id, :credentials => @credentials, :project => habitat_id, # XXX this belongs in our required_instance_methods hack :flags => @config } @cloudparentclass.required_instance_methods.each { |m| # if respond_to?(m) # args[m] = method(m).call # else args[m] = instance_variable_get(("@"+m.to_s).to_sym) # end } matches = self.class.find(args) if !matches.nil? and matches.is_a?(Hash) # XXX or if the hash is keyed with an ::Id element, oh boy # puts matches[@cloud_id][:self_link] # puts matches[@cloud_id][:url] # if matches[@cloud_id][:self_link] # @url ||= matches[@cloud_id][:self_link] # elsif matches[@cloud_id][:url] # @url ||= matches[@cloud_id][:url] # elsif matches[@cloud_id][:arn] # @arn ||= matches[@cloud_id][:arn] # end if matches[@cloud_id] @cloud_desc_cache = matches[@cloud_id] else matches.each_pair { |k, v| # flatten out ::Id objects just in case if @cloud_id.to_s == k.to_s @cloud_desc_cache = v break end } end end if !@cloud_desc_cache MU.log "cloud_desc via #{self.class.name}.find() failed to locate a live object.\nWas called by #{caller(1..1)}", MU::WARN, details: args end rescue StandardError => e MU.log "Got #{e.inspect} trying to find cloud handle for #{self.class.shortname} #{@mu_name} (#{@cloud_id})", MU::WARN raise e end end return @cloud_desc_cache end # Retrieve all of the known metadata for this resource. # @param cloud_id [String]: The cloud platform's identifier for the resource we're describing. Makes lookups more efficient. # @return [Array]: mu_name, config, deploydata def describe(cloud_id: nil) if cloud_id.nil? and !@cloudobj.nil? @cloud_id ||= @cloudobj.cloud_id end res_type = self.class.cfg_plural res_name = @config['name'] if !@config.nil? @credentials ||= @config['credentials'] if !@config.nil? deploydata = nil if !@deploy.nil? and @deploy.is_a?(MU::MommaCat) and !@deploy.deployment.nil? and !@deploy.deployment[res_type].nil? and !@deploy.deployment[res_type][res_name].nil? deploydata = @deploy.deployment[res_type][res_name] else # XXX This should only happen on a brand new resource, but we should # probably complain under other circumstances, if we can # differentiate them. end if self.class.has_multiples and !@mu_name.nil? and deploydata.is_a?(Hash) and deploydata.has_key?(@mu_name) @deploydata = deploydata[@mu_name] elsif deploydata.is_a?(Hash) @deploydata = deploydata end if @cloud_id.nil? and @deploydata.is_a?(Hash) if @mu_name.nil? and @deploydata.has_key?('#MU_NAME') @mu_name = @deploydata['#MU_NAME'] end if @deploydata.has_key?('cloud_id') @cloud_id ||= @deploydata['cloud_id'] end end return [@mu_name, @config, @deploydata] end # Fetch MU::Cloud objects for each of this object's dependencies, and # return in an easily-navigable Hash. This can include things listed in # @config['dependencies'], implicitly-defined dependencies such as # add_firewall_rules or vpc stanzas, and may refer to objects internal # to this deployment or external. Will populate the instance variables # @dependencies (general dependencies, which can only be sibling # resources in this deployment), as well as for certain config stanzas # which can refer to external resources (@vpc, @loadbalancers, # @add_firewall_rules) def dependencies(use_cache: false, debug: false) @dependencies ||= {} @loadbalancers ||= [] @firewall_rules ||= [] if @config.nil? return [@dependencies, @vpc, @loadbalancers] end if use_cache and @dependencies.size > 0 return [@dependencies, @vpc, @loadbalancers] end @config['dependencies'] = [] if @config['dependencies'].nil? loglevel = debug ? MU::NOTICE : MU::DEBUG # First, general dependencies. These should all be fellow members of # the current deployment. @config['dependencies'].each { |dep| @dependencies[dep['type']] ||= {} next if @dependencies[dep['type']].has_key?(dep['name']) handle = @deploy.findLitterMate(type: dep['type'], name: dep['name']) if !@deploy.nil? if !handle.nil? MU.log "Loaded dependency for #{self}: #{dep['name']} => #{handle}", loglevel @dependencies[dep['type']][dep['name']] = handle else # XXX yell under circumstances where we should expect to have # our stuff available already? end } # Special dependencies: my containing VPC if self.class.can_live_in_vpc and !@config['vpc'].nil? @config['vpc']["id"] ||= @config['vpc']["vpc_id"] # old deploys @config['vpc']["name"] ||= @config['vpc']["vpc_name"] # old deploys # If something hash-ified a MU::Config::Ref here, fix it if !@config['vpc']["id"].nil? and @config['vpc']["id"].is_a?(Hash) @config['vpc']["id"] = MU::Config::Ref.new(@config['vpc']["id"]) end if !@config['vpc']["id"].nil? if @config['vpc']["id"].is_a?(MU::Config::Ref) and !@config['vpc']["id"].kitten.nil? @vpc = @config['vpc']["id"].kitten(@deploy) else if @config['vpc']['habitat'] @config['vpc']['habitat'] = MU::Config::Ref.get(@config['vpc']['habitat']) end vpc_ref = MU::Config::Ref.get(@config['vpc']) @vpc = vpc_ref.kitten(@deploy) end elsif !@config['vpc']["name"].nil? and @deploy MU.log "Attempting findLitterMate on VPC for #{self}", loglevel, details: @config['vpc'] sib_by_name = @deploy.findLitterMate(name: @config['vpc']['name'], type: "vpcs", return_all: true, habitat: @config['vpc']['project'], debug: debug) if sib_by_name.is_a?(Hash) if sib_by_name.size == 1 @vpc = sib_by_name.values.first MU.log "Single VPC match for #{self}", loglevel, details: @vpc.to_s else # XXX ok but this is the wrong place for this really the config parser needs to sort this out somehow # we got multiple matches, try to pick one by preferred subnet # behavior MU.log "Sorting a bunch of VPC matches for #{self}", loglevel, details: sib_by_name.map { |s| s.to_s }.join(", ") sib_by_name.values.each { |sibling| all_private = sibling.subnets.map { |s| s.private? }.all?(true) all_public = sibling.subnets.map { |s| s.private? }.all?(false) names = sibling.subnets.map { |s| s.name } ids = sibling.subnets.map { |s| s.cloud_id } if all_private and ["private", "all_private"].include?(@config['vpc']['subnet_pref']) @vpc = sibling break elsif all_public and ["public", "all_public"].include?(@config['vpc']['subnet_pref']) @vpc = sibling break elsif @config['vpc']['subnet_name'] and names.include?(@config['vpc']['subnet_name']) #puts "CHOOSING #{@vpc.to_s} 'cause it has #{@config['vpc']['subnet_name']}" @vpc = sibling break elsif @config['vpc']['subnet_id'] and ids.include?(@config['vpc']['subnet_id']) @vpc = sibling break end } if !@vpc sibling = sib_by_name.values.sample MU.log "Got multiple matching VPCs for #{self.class.cfg_name} #{@mu_name}, so I'm arbitrarily choosing #{sibling.mu_name}", MU::WARN, details: @config['vpc'] @vpc = sibling end end else @vpc = sib_by_name MU.log "Found exact VPC match for #{self}", loglevel, details: sib_by_name.to_s end else MU.log "No shortcuts available to fetch VPC for #{self}", loglevel, details: @config['vpc'] end if !@vpc and !@config['vpc']["name"].nil? and @dependencies.has_key?("vpc") and @dependencies["vpc"].has_key?(@config['vpc']["name"]) MU.log "Grabbing VPC I see in @dependencies['vpc']['#{@config['vpc']["name"]}'] for #{self}", loglevel, details: @config['vpc'] @vpc = @dependencies["vpc"][@config['vpc']["name"]] elsif !@vpc tag_key, tag_value = @config['vpc']['tag'].split(/=/, 2) if !@config['vpc']['tag'].nil? if !@config['vpc'].has_key?("id") and !@config['vpc'].has_key?("deploy_id") and !@deploy.nil? @config['vpc']["deploy_id"] = @deploy.deploy_id end MU.log "Doing findStray for VPC for #{self}", loglevel, details: @config['vpc'] vpcs = MU::MommaCat.findStray( @config['cloud'], "vpc", deploy_id: @config['vpc']["deploy_id"], cloud_id: @config['vpc']["id"], name: @config['vpc']["name"], tag_key: tag_key, tag_value: tag_value, habitats: [@project_id], region: @config['vpc']["region"], calling_deploy: @deploy, credentials: @credentials, dummy_ok: true, debug: debug ) @vpc = vpcs.first if !vpcs.nil? and vpcs.size > 0 end if @vpc and @vpc.config and @vpc.config['bastion'] and @vpc.config['bastion'].to_h['name'] != @config['name'] refhash = @vpc.config['bastion'].to_h refhash['deploy_id'] ||= @vpc.deploy.deploy_id natref = MU::Config::Ref.get(refhash) if natref and natref.kitten(@vpc.deploy) @nat = natref.kitten(@vpc.deploy) end end if @nat.nil? and !@vpc.nil? and ( @config['vpc'].has_key?("nat_host_id") or @config['vpc'].has_key?("nat_host_tag") or @config['vpc'].has_key?("nat_host_ip") or @config['vpc'].has_key?("nat_host_name") ) nat_tag_key, nat_tag_value = @config['vpc']['nat_host_tag'].split(/=/, 2) if !@config['vpc']['nat_host_tag'].nil? @nat = @vpc.findBastion( nat_name: @config['vpc']['nat_host_name'], nat_cloud_id: @config['vpc']['nat_host_id'], nat_tag_key: nat_tag_key, nat_tag_value: nat_tag_value, nat_ip: @config['vpc']['nat_host_ip'] ) if @nat.nil? if !@vpc.cloud_desc.nil? @nat = @vpc.findNat( nat_cloud_id: @config['vpc']['nat_host_id'], nat_filter_key: "vpc-id", region: @config['vpc']["region"], nat_filter_value: @vpc.cloud_id, credentials: @config['credentials'] ) else @nat = @vpc.findNat( nat_cloud_id: @config['vpc']['nat_host_id'], region: @config['vpc']["region"], credentials: @config['credentials'] ) end end end if @vpc.nil? and @config['vpc'] feck = MU::Config::Ref.get(@config['vpc']) feck.kitten(@deploy, debug: true) pp feck raise MuError.new "#{self.class.cfg_name} #{@config['name']} failed to locate its VPC", details: @config['vpc'] end elsif self.class.cfg_name == "vpc" @vpc = self end # Google accounts usually have a useful default VPC we can use if @vpc.nil? and @project_id and @cloud == "Google" and self.class.can_live_in_vpc MU.log "Seeing about default VPC for #{self}", MU::NOTICE vpcs = MU::MommaCat.findStray( "Google", "vpc", cloud_id: "default", habitats: [@project_id], credentials: @credentials, dummy_ok: true, debug: debug ) @vpc = vpcs.first if !vpcs.nil? and vpcs.size > 0 end # Special dependencies: LoadBalancers I've asked to attach to an # instance. if @config.has_key?("loadbalancers") @loadbalancers = [] if !@loadbalancers @config['loadbalancers'].each { |lb| MU.log "Loading LoadBalancer for #{self}", MU::DEBUG, details: lb if @dependencies.has_key?("loadbalancer") and @dependencies["loadbalancer"].has_key?(lb['concurrent_load_balancer']) @loadbalancers << @dependencies["loadbalancer"][lb['concurrent_load_balancer']] else if !lb.has_key?("existing_load_balancer") and !lb.has_key?("deploy_id") and !@deploy.nil? lb["deploy_id"] = @deploy.deploy_id end lbs = MU::MommaCat.findStray( @config['cloud'], "loadbalancer", deploy_id: lb["deploy_id"], cloud_id: lb['existing_load_balancer'], name: lb['concurrent_load_balancer'], region: @config["region"], calling_deploy: @deploy, dummy_ok: true ) @loadbalancers << lbs.first if !lbs.nil? and lbs.size > 0 end } end # Munge in external resources referenced by the existing_deploys # keyword if @config["existing_deploys"] && !@config["existing_deploys"].empty? @config["existing_deploys"].each { |ext_deploy| if ext_deploy["cloud_id"] found = MU::MommaCat.findStray( @config['cloud'], ext_deploy["cloud_type"], cloud_id: ext_deploy["cloud_id"], region: @config['region'], dummy_ok: false ).first MU.log "Couldn't find existing resource #{ext_deploy["cloud_id"]}, #{ext_deploy["cloud_type"]}", MU::ERR if found.nil? @deploy.notify(ext_deploy["cloud_type"], found.config["name"], found.deploydata, mu_name: found.mu_name, triggering_node: @mu_name) elsif ext_deploy["mu_name"] && ext_deploy["deploy_id"] MU.log "#{ext_deploy["mu_name"]} / #{ext_deploy["deploy_id"]}" found = MU::MommaCat.findStray( @config['cloud'], ext_deploy["cloud_type"], deploy_id: ext_deploy["deploy_id"], mu_name: ext_deploy["mu_name"], region: @config['region'], dummy_ok: false ).first MU.log "Couldn't find existing resource #{ext_deploy["mu_name"]}/#{ext_deploy["deploy_id"]}, #{ext_deploy["cloud_type"]}", MU::ERR if found.nil? @deploy.notify(ext_deploy["cloud_type"], found.config["name"], found.deploydata, mu_name: ext_deploy["mu_name"], triggering_node: @mu_name) else MU.log "Trying to find existing deploy, but either the cloud_id is not valid or no mu_name and deploy_id where provided", MU::ERR end } end if @config['dns_records'] && !@config['dns_records'].empty? @config['dns_records'].each { |dnsrec| if dnsrec.has_key?("name") if dnsrec['name'].start_with?(@deploy.deploy_id.downcase) && !dnsrec['name'].start_with?(@mu_name.downcase) MU.log "DNS records for #{@mu_name} seem to be wrong, deleting from current config", MU::WARN, details: dnsrec dnsrec.delete('name') dnsrec.delete('target') end end } end return [@dependencies, @vpc, @loadbalancers] end # Using the automatically-defined +@vpc+ from {dependencies} in # conjunction with our config, return our configured subnets. # @return [Array] def mySubnets dependencies if !@vpc or !@config["vpc"] return nil end if @config["vpc"]["subnet_id"] or @config["vpc"]["subnet_name"] @config["vpc"]["subnets"] ||= [] subnet_block = {} subnet_block["subnet_id"] = @config["vpc"]["subnet_id"] if @config["vpc"]["subnet_id"] subnet_block["subnet_name"] = @config["vpc"]["subnet_name"] if @config["vpc"]["subnet_name"] @config["vpc"]["subnets"] << subnet_block @config["vpc"]["subnets"].uniq! end if (!@config["vpc"]["subnets"] or @config["vpc"]["subnets"].empty?) and !@config["vpc"]["subnet_id"] return @vpc.subnets end subnets = [] @config["vpc"]["subnets"].each { |subnet| subnet_obj = @vpc.getSubnet(cloud_id: subnet["subnet_id"].to_s, name: subnet["subnet_name"].to_s) raise MuError.new "Couldn't find a live subnet for #{self} matching #{subnet} in #{@vpc}", details: @vpc.subnets.map { |s| s.name }.join(",") if subnet_obj.nil? subnets << subnet_obj } subnets end # @return [Array] def myFirewallRules dependencies rules = [] if @dependencies.has_key?("firewall_rule") rules = @dependencies['firewall_rule'].values end # XXX what other ways are these specified? rules end # If applicable, allow this resource's NAT host blanket access via # rules in its associated +admin+ firewall rule set. def allowBastionAccess return nil if !@nat or !@nat.is_a?(MU::Cloud::Server) myFirewallRules.each { |acl| if acl.config["admin"] acl.addRule(@nat.listIPs, proto: "tcp") acl.addRule(@nat.listIPs, proto: "udp") acl.addRule(@nat.listIPs, proto: "icmp") end } end # A hook that is always called just before each instance method is # invoked, so that we can ensure that repetitive setup tasks (like # resolving +:resource_group+ for Azure resources) have always been # done. def resourceInitHook @cloud ||= cloud if @cloudparentclass.respond_to?(:resourceInitHook) @cloudparentclass.resourceInitHook(@cloudobj, @deploy) end end if File.exist?(MU.myRoot+"/modules/mu/cloud/#{cfg_name}.rb") require "mu/cloud/#{cfg_name}" end # Wrap the instance methods that this cloud resource type has to # implement. MU::Cloud.resource_types[name.to_sym][:instance].each { |method| define_method method do |*args| return nil if @cloudobj.nil? MU.log "Invoking #{@cloudobj}.#{method}", MU::DEBUG # Go ahead and guarantee that we can't accidentally trigger these # methods recursively. @method_semaphore.synchronize { # We're looking for recursion, not contention, so ignore some # obviously harmless things. if @method_locks.has_key?(method) and method != :findBastion and method != :cloud_id MU.log "Double-call to cloud method #{method} for #{self}", MU::DEBUG, details: caller + ["competing call stack:"] + @method_locks[method] end @method_locks[method] = caller } # Make sure the describe() caches are fresh @cloudobj.describe if method != :describe # Don't run through dependencies on simple attr_reader lookups if ![:dependencies, :cloud_id, :config, :mu_name].include?(method) @cloudobj.dependencies end retval = nil if !args.nil? and args.size == 1 retval = @cloudobj.method(method).call(args.first) elsif !args.nil? and args.size > 0 retval = @cloudobj.method(method).call(*args) else retval = @cloudobj.method(method).call end if [:create, :groom, :postBoot, :toKitten].include?(method) and (!@destroyed and !@cloudobj.destroyed) deploydata = @cloudobj.method(:notify).call @deploydata ||= deploydata # XXX I don't remember why we're not just doing this from the get-go; maybe because we prefer some mangling occurring in @deploy.notify? if deploydata.nil? or !deploydata.is_a?(Hash) MU.log "#{self} notify method did not return a Hash of deployment data, attempting to fill in with cloud descriptor #{@cloudobj.cloud_id}", MU::WARN deploydata = MU.structToHash(@cloudobj.cloud_desc) raise MuError, "Failed to collect metadata about #{self}" if deploydata.nil? end deploydata['cloud_id'] ||= @cloudobj.cloud_id if !@cloudobj.cloud_id.nil? deploydata['mu_name'] = @cloudobj.mu_name if !@cloudobj.mu_name.nil? deploydata['nodename'] = @cloudobj.mu_name if !@cloudobj.mu_name.nil? deploydata.delete("#MUOBJECT") @deploy.notify(self.class.cfg_plural, @config['name'], deploydata, triggering_node: @cloudobj, delayed_save: @delayed_save) if !@deploy.nil? elsif method == :notify if retval.nil? MU.log self.to_s+" didn't return any metadata from notify", MU::WARN, details: @cloudobj.cloud_desc else retval['cloud_id'] = @cloudobj.cloud_id.to_s if !@cloudobj.cloud_id.nil? retval['mu_name'] = @cloudobj.mu_name if !@cloudobj.mu_name.nil? @deploy.notify(self.class.cfg_plural, @config['name'], retval, triggering_node: @cloudobj, delayed_save: @delayed_save) if !@deploy.nil? end end @method_semaphore.synchronize { @method_locks.delete(method) } @deploydata = @cloudobj.deploydata @config = @cloudobj.config retval end } # end instance method list } # end dynamic class generation block } # end resource type iteration require 'mu/cloud/winrm_sessions' require 'mu/cloud/ssh_sessions' end end