modules/mu/cloud/resource_base.rb
# 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<Hash>]: 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 "#{self}: Importing metadata for #{ext_deploy["cloud_type"]} #{ext_deploy["mu_name"]} from #{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
if found.nil?
MU.log "Couldn't find existing resource #{ext_deploy["mu_name"]}/#{ext_deploy["deploy_id"]}, #{ext_deploy["cloud_type"]}", MU::ERR
else
@deploy.notify(ext_deploy["cloud_type"], found.config["name"], found.deploydata, mu_name: ext_deploy["mu_name"], triggering_node: @mu_name)
end
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<MU::Cloud::VPC::Subnet>]
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<MU::Cloud::FirewallRule>]
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 = MU::Config.manxify(@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