modules/mu/cloud.rb in cloud-mu-2.1.0beta vs modules/mu/cloud.rb in cloud-mu-3.0.0beta
- old
+ new
@@ -37,17 +37,45 @@
# Exception thrown when a request is made for an unsupported flag or feature
# in a cloud resource.
class MuCloudFlagNotImplemented < StandardError;
end
+ # Exception we throw when we attempt to make an API call against a project
+ # that is already deleted.
+ class MuDefunctHabitat < StandardError;
+ end
+
# Methods which a cloud resource implementation, e.g. Server, must implement
generic_class_methods = [:find, :cleanup, :validateConfig, :schema, :isGlobal?]
generic_instance_methods = [:create, :notify, :mu_name, :cloud_id, :config]
# Class methods which the base of a cloud implementation must implement
- generic_class_methods_toplevel = [:required_instance_methods, :myRegion, :listRegions, :listAZs, :hosted?, :hosted_config, :config_example, :writeDeploySecret, :listCredentials, :credConfig, :listInstanceTypes, :adminBucketName, :adminBucketUrl]
+ generic_class_methods_toplevel = [:required_instance_methods, :myRegion, :listRegions, :listAZs, :hosted?, :hosted_config, :config_example, :writeDeploySecret, :listCredentials, :credConfig, :listInstanceTypes, :adminBucketName, :adminBucketUrl, :habitat]
+ # Public attributes which will be available on all instantiated cloud resource objects
+ #
+ # +:config+: The fully-resolved {MU::Config} hash describing the object, aka the Basket of Kittens entry
+ #
+ # +:mu_name+: The unique internal name of the object, if one already exists
+ #
+ # +:cloud+: The cloud in which this object is resident
+ #
+ # +:cloud_id+: The cloud provider's official identifier for this object
+ #
+ # +:environment+: The declared environment string for the deployment of which this object is a member
+ #
+ # +:deploy:+ The {MU::MommaCat} object representing the deployment of which this object is a member
+ #
+ # +:deploy_id:+ The unique string which identifies the deployment of which this object is a member
+ #
+ # +:deploydata:+ A Hash containing all metadata reported by resources in this deploy method, via their +notify+ methods
+ #
+ # +:appname:+ The declared application name of this deployment
+ #
+ # +:credentials:+ The name of the cloud provider credential set from +mu.yaml+ which is used to manage this object
+ PUBLIC_ATTRS = [:config, :mu_name, :cloud, :cloud_id, :environment, :deploy, :deploy_id, :deploydata, :appname, :credentials]
+
# Initialize empty classes for each of these. We'll fill them with code
# later; we're doing this here because otherwise the parser yells about
# missing classes, even though they're created at runtime.
# Stub base class; real implementations generated at runtime
@@ -159,11 +187,11 @@
:cfg_name => "habitat",
:cfg_plural => "habitats",
:interface => self.const_get("Habitat"),
:deps_wait_on_my_creation => true,
:waits_on_parent_completion => true,
- :class => generic_class_methods,
+ :class => generic_class_methods + [:isLive?],
:instance => generic_instance_methods + [:groom]
},
:Collection => {
:has_multiples => false,
:can_live_in_vpc => false,
@@ -215,22 +243,22 @@
:cfg_plural => "loadbalancers",
:interface => self.const_get("LoadBalancer"),
:deps_wait_on_my_creation => true,
:waits_on_parent_completion => false,
:class => generic_class_methods,
- :instance => generic_instance_methods + [:registerNode]
+ :instance => generic_instance_methods + [:groom, :registerNode]
},
:Server => {
:has_multiples => true,
:can_live_in_vpc => true,
:cfg_name => "server",
:cfg_plural => "servers",
:interface => self.const_get("Server"),
:deps_wait_on_my_creation => false,
:waits_on_parent_completion => false,
- :class => generic_class_methods + [:validateInstanceType],
- :instance => generic_instance_methods + [:groom, :postBoot, :getSSHConfig, :canonicalIP, :getWindowsAdminPassword, :active?, :groomer, :mu_windows_name, :mu_windows_name=, :reboot, :addVolume]
+ :class => generic_class_methods + [:validateInstanceType, :imageTimeStamp],
+ :instance => generic_instance_methods + [:groom, :postBoot, :getSSHConfig, :canonicalIP, :getWindowsAdminPassword, :active?, :groomer, :mu_windows_name, :mu_windows_name=, :reboot, :addVolume, :genericNAT]
},
:ServerPool => {
:has_multiples => false,
:can_live_in_vpc => true,
:cfg_name => "server_pool",
@@ -401,12 +429,12 @@
:cfg_name => "bucket",
:cfg_plural => "buckets",
:interface => self.const_get("Bucket"),
:deps_wait_on_my_creation => true,
:waits_on_parent_completion => true,
- :class => generic_class_methods,
- :instance => generic_instance_methods + [:groom]
+ :class => generic_class_methods + [:upload],
+ :instance => generic_instance_methods + [:groom, :upload]
},
:NoSQLDB => {
:has_multiples => false,
:can_live_in_vpc => false,
:cfg_name => "nosqldb",
@@ -417,20 +445,210 @@
:class => generic_class_methods,
:instance => generic_instance_methods + [:groom]
}
}.freeze
+ # The public AWS S3 bucket where we expect to find YAML files listing our
+ # standard base images for various platforms.
+ BASE_IMAGE_BUCKET = "cloudamatic"
+ # The path in the AWS S3 bucket where we expect to find YAML files listing
+ # our standard base images for various platforms.
+ BASE_IMAGE_PATH = "/images"
+ # Aliases for platform names, in case we don't have actual images built for
+ # them.
+ PLATFORM_ALIASES = {
+ "linux" => "centos7",
+ "windows" => "win2k12r2",
+ "win2k12" => "win2k12r2",
+ "ubuntu" => "ubuntu16",
+ "centos" => "centos7",
+ "rhel7" => "rhel71",
+ "rhel" => "rhel71",
+ "amazon" => "amazon2016"
+ }
+
+ @@image_fetch_cache = {}
+ @@platform_cache = []
+ @@image_fetch_semaphore = Mutex.new
+
+ # Rifle our image lists from {MU::Cloud.getStockImage} and return a list
+ # of valid +platform+ names.
+ # @return [Array<String>]
+ def self.listPlatforms
+ return @@platform_cache if @@platform_cache and !@@platform_cache.empty?
+ @@platform_cache = MU::Cloud.supportedClouds.map { |cloud|
+ begin
+ loadCloudType(cloud, :Server)
+ rescue MU::Cloud::MuCloudResourceNotImplemented, MU::MuError => e
+ next
+ end
+
+ images = MU::Cloud.getStockImage(cloud, quiet: true)
+ if images
+ images.keys
+ else
+ nil
+ end
+ }.flatten.uniq
+ @@platform_cache.delete(nil)
+ @@platform_cache.sort
+ @@platform_cache
+ end
+
+ # Locate a base image for a {MU::Cloud::Server} resource. First we check
+ # Mu's public bucket, which should list the latest and greatest. If we can't
+ # fetch that, then we fall back to a YAML file that's bundled as part of Mu,
+ # but which will typically be less up-to-date.
+ # @param cloud [String]: The cloud provider for which to return an image list
+ # @param platform [String]: The supported platform for which to return an image or images. If not specified, we'll return our entire library for the appropriate cloud provider.
+ # @param region [String]: The region for which the returned image or images should be supported, for cloud providers which require it (such as AWS).
+ # @param fail_hard [Boolean]: Raise an exception on most errors, such as an inability to reach our public listing, lack of matching images, etc.
+ # @return [Hash,String,nil]
+ def self.getStockImage(cloud = MU::Config.defaultCloud, platform: nil, region: nil, fail_hard: false, quiet: false)
+
+ if !MU::Cloud.supportedClouds.include?(cloud)
+ MU.log "'#{cloud}' is not a supported cloud provider! Available providers:", MU::ERR, details: MU::Cloud.supportedClouds
+ raise MuError, "'#{cloud}' is not a supported cloud provider!"
+ end
+
+ urls = ["http://"+BASE_IMAGE_BUCKET+".s3-website-us-east-1.amazonaws.com"+BASE_IMAGE_PATH]
+ if $MU_CFG and $MU_CFG['custom_images_url']
+ urls << $MU_CFG['custom_images_url']
+ end
+
+ images = nil
+ urls.each { |base_url|
+ @@image_fetch_semaphore.synchronize {
+ if @@image_fetch_cache[cloud] and (Time.now - @@image_fetch_cache[cloud]['time']) < 30
+ images = @@image_fetch_cache[cloud]['contents'].dup
+ else
+ begin
+ Timeout.timeout(2) do
+ response = open("#{base_url}/#{cloud}.yaml").read
+ images ||= {}
+ images.deep_merge!(YAML.load(response))
+ break
+ end
+ rescue Exception => e
+ if fail_hard
+ raise MuError, "Failed to fetch stock images from #{base_url}/#{cloud}.yaml (#{e.message})"
+ else
+ MU.log "Failed to fetch stock images from #{base_url}/#{cloud}.yaml (#{e.message})", MU::WARN if !quiet
+ end
+ end
+ end
+ }
+ }
+
+ @@image_fetch_semaphore.synchronize {
+ @@image_fetch_cache[cloud] = {
+ 'contents' => images.dup,
+ 'time' => Time.now
+ }
+ }
+
+ backwards_compat = {
+ "AWS" => "amazon_images",
+ "Google" => "google_images",
+ }
+
+ # Load from inside our repository, if we didn't get images elsewise
+ if images.nil?
+ [backwards_compat[cloud], cloud].each { |file|
+ next if file.nil?
+ if File.exist?("#{MU.myRoot}/modules/mu/defaults/#{file}.yaml")
+ images = YAML.load(File.read("#{MU.myRoot}/modules/mu/defaults/#{file}.yaml"))
+ break
+ end
+ }
+ end
+
+ # Now overlay local overrides, both of the systemwide (/opt/mu/etc) and
+ # per-user (~/.mu/etc) variety.
+ [backwards_compat[cloud], cloud].each { |file|
+ next if file.nil?
+ if File.exist?("#{MU.etcDir}/#{file}.yaml")
+ images ||= {}
+ images.deep_merge!(YAML.load(File.read("#{MU.etcDir}/#{file}.yaml")))
+ end
+ if Process.uid != 0
+ basepath = Etc.getpwuid(Process.uid).dir+"/.mu/etc"
+ if File.exist?("#{basepath}/#{file}.yaml")
+ images ||= {}
+ images.deep_merge!(YAML.load(File.read("#{basepath}/#{file}.yaml")))
+ end
+ end
+ }
+
+ if images.nil?
+ if fail_hard
+ raise MuError, "Failed to find any base images for #{cloud}"
+ else
+ MU.log "Failed to find any base images for #{cloud}", MU::WARN if !quiet
+ return nil
+ end
+ end
+
+ PLATFORM_ALIASES.each_pair { |a, t|
+ if images[t] and !images[a]
+ images[a] = images[t]
+ end
+ }
+
+ if platform
+ if !images[platform]
+ if fail_hard
+ raise MuError, "No base image for platform #{platform} in cloud #{cloud}"
+ else
+ MU.log "No base image for platform #{platform} in cloud #{cloud}", MU::WARN if !quiet
+ return nil
+ end
+ end
+ images = images[platform]
+
+ if region
+ # We won't fuss about the region argument if this isn't a cloud that
+ # has regions, just quietly don't bother.
+ if images.is_a?(Hash)
+ if images[region]
+ images = images[region]
+ else
+ if fail_hard
+ raise MuError, "No base image for platform #{platform} in cloud #{cloud} region #{region} found"
+ else
+ MU.log "No base image for platform #{platform} in cloud #{cloud} region #{region} found", MU::WARN if !quiet
+ return nil
+ end
+ end
+ end
+ end
+ else
+ if region
+ images.each_pair { |p, regions|
+ # Filter to match our requested region, but for all the platforms,
+ # since we didn't specify one.
+ if regions.is_a?(Hash)
+ regions.delete_if { |r| r != region }
+ end
+ }
+ end
+ end
+
+ images
+ end
+
# A list of supported cloud resource types as Mu classes
def self.resource_types;
@@resource_types
end
# Shorthand lookup for resource type names. Given any of the shorthand class name, configuration name (singular or plural), or full class name, return all four as a set.
# @param type [String]: A string that looks like our short or full class name or singular or plural configuration names.
# @return [Array]: Class name (Symbol), singular config name (String), plural config name (String), full class name (Object)
def self.getResourceNames(type)
+ return [nil, nil, nil, nil, {}] if !type
@@resource_types.each_pair { |name, cloudclass|
if name == type.to_sym or
cloudclass[:cfg_name] == type or
cloudclass[:cfg_plural] == type or
Object.const_get("MU").const_get("Cloud").const_get(name) == type
@@ -460,14 +678,32 @@
# List of known/supported Cloud providers. This may be modified at runtime
# if an implemention is defective or missing required methods.
@@supportedCloudList = ['AWS', 'CloudFormation', 'Google', 'Azure']
# List of known/supported Cloud providers
+ # @return [Array<String>]
def self.supportedClouds
@@supportedCloudList
end
+ # List of known/supported Cloud providers for which we have at least one
+ # set of credentials configured.
+ # @return [Array<String>]
+ def self.availableClouds
+ available = []
+ MU::Cloud.supportedClouds.each { |cloud|
+ begin
+ cloudbase = Object.const_get("MU").const_get("Cloud").const_get(cloud)
+ next if cloudbase.listCredentials.nil? or cloudbase.listCredentials.empty?
+ available << cloud
+ rescue NameError
+ end
+ }
+
+ available
+ end
+
# Load the container class for each cloud we know about, and inject autoload
# code for each of its supported resource type classes.
failed = []
MU::Cloud.supportedClouds.each { |cloud|
require "mu/clouds/#{cloud.downcase}"
@@ -493,22 +729,28 @@
# variable are dangerous without cleaning. Clean them.
# @param platform [String]: The target OS.
# @param template_variables [Hash]: A list of variable substitutions to pass as globals to the ERB parser when loading the userdata script.
# @param custom_append [String]: Arbitrary extra code to append to our default userdata behavior.
# @return [String]
- def self.fetchUserdata(platform: "linux", template_variables: {}, custom_append: nil, cloud: "aws", scrub_mu_isms: false)
+ def self.fetchUserdata(platform: "linux", template_variables: {}, custom_append: nil, cloud: "AWS", scrub_mu_isms: false, credentials: nil)
return nil if platform.nil? or platform.empty?
userdata_mutex.synchronize {
script = ""
if !scrub_mu_isms
if template_variables.nil? or !template_variables.is_a?(Hash)
raise MuError, "My second argument should be a hash of variables to pass into ERB templates"
end
+ template_variables["credentials"] ||= credentials
$mu = OpenStruct.new(template_variables)
- userdata_dir = File.expand_path(MU.myRoot+"/modules/mu/clouds/#{cloud}/userdata")
- platform = "linux" if %w{centos centos6 centos7 ubuntu ubuntu14 rhel rhel7 rhel71 amazon}.include? platform
- platform = "windows" if %w{win2k12r2 win2k12 win2k8 win2k8r2 win2k16}.include? platform
+ userdata_dir = File.expand_path(MU.myRoot+"/modules/mu/clouds/#{cloud.downcase}/userdata")
+
+ platform = if %w{win2k12r2 win2k12 win2k8 win2k8r2 win2k16 windows win2k19}.include?(platform)
+ "windows"
+ else
+ "linux"
+ end
+
erbfile = "#{userdata_dir}/#{platform}.erb"
if !File.exist?(erbfile)
MU.log "No such userdata template '#{erbfile}'", MU::WARN, details: caller
return ""
end
@@ -588,24 +830,24 @@
raise MuError, "MU::Cloud::#{cloud}::#{type} has not implemented required class method #{class_method}"
end
}
@@resource_types[type.to_sym][:instance].each { |instance_method|
if !myclass.public_instance_methods.include?(instance_method)
- raise MuError, "MU::Cloud::#{cloud}::#{type} has not implemented required instance method #{instance_method}"
+ raise MuCloudResourceNotImplemented, "MU::Cloud::#{cloud}::#{type} has not implemented required instance method #{instance_method}"
end
}
cloudclass.required_instance_methods.each { |instance_method|
if !myclass.public_instance_methods.include?(instance_method)
- raise MuError, "MU::Cloud::#{cloud}::#{type} has not implemented required instance method #{instance_method}"
+ MU.log "MU::Cloud::#{cloud}::#{type} has not implemented required instance method #{instance_method}, will declare as attr_accessor", MU::DEBUG
end
}
@cloud_class_cache[cloud][type] = myclass
return myclass
rescue NameError => e
@cloud_class_cache[cloud][type] = nil
- raise MuError, "The '#{type}' resource is not supported in cloud #{cloud} (tried MU::#{cloud}::#{type})", e.backtrace
+ raise MuCloudResourceNotImplemented, "The '#{type}' resource is not supported in cloud #{cloud} (tried MU::#{cloud}::#{type})", e.backtrace
end
end
MU::Cloud.supportedClouds.each { |cloud|
Object.const_get("MU").const_get("Cloud").const_get(cloud).class_eval {
@@ -622,24 +864,13 @@
}
}
@@resource_types.each_pair { |name, attrs|
Object.const_get("MU").const_get("Cloud").const_get(name).class_eval {
- attr_reader :cloud
- attr_reader :environment
attr_reader :cloudclass
attr_reader :cloudobj
- attr_reader :deploy_id
- attr_reader :mu_name
- attr_reader :cloud_id
- attr_reader :credentials
- attr_reader :url
- attr_reader :config
- attr_reader :deploydata
attr_reader :destroyed
- attr_reader :cfm_template
- attr_reader :cfm_name
attr_reader :delayed_save
def self.shortname
name.sub(/.*?::([^:]+)$/, '\1')
end
@@ -666,15 +897,10 @@
def self.deps_wait_on_my_creation
MU::Cloud.resource_types[shortname.to_sym][:deps_wait_on_my_creation]
end
- def groomer
- return @cloudobj.groomer if !@cloudobj.nil?
- nil
- end
-
# 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
@@ -686,90 +912,236 @@
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}", MU::DEBUG
+ @deploy = mommacat
+ @deploy_id = @deploy.deploy_id
+ @cloudobj.intoDeploy(mommacat, force: force) if @cloudobj
+ end
+ @deploy_id
+ 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(mommacat: nil,
- mu_name: nil,
- cloud_id: nil,
- credentials: nil,
- kitten_cfg: nil,
- delayed_save: false)
- raise MuError, "Cannot invoke Cloud objects without a configuration" if kitten_cfg.nil?
- @live = true
- @deploy = mommacat
- @config = kitten_cfg
- @delayed_save = delayed_save
- @cloud_id = cloud_id
- @credentials = credentials
- @credentials ||= kitten_cfg['credentials']
+ def initialize(**args)
+ raise MuError, "Cannot invoke Cloud objects without a configuration" if args[:kitten_cfg].nil?
- if !@deploy.nil?
- @deploy_id = @deploy.deploy_id
- MU.log "Initializing an instance of #{self.class.name} in #{@deploy_id} #{mu_name}", MU::DEBUG, details: kitten_cfg
- elsif mu_name.nil?
- raise MuError, "Can't instantiate a MU::Cloud object with a live deploy or giving us a mu_name"
+ # We are a parent wrapper object. Initialize our child object and
+ # housekeeping bits accordingly.
+ if self.class.name.match(/^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 !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.loadCloudType(my_cloud, self.class.shortname)
+ @cloudparentclass = Object.const_get("MU").const_get("Cloud").const_get(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|
+ instance_variable_set(("@"+a.to_s).to_sym, @cloudobj.send(a))
+ }
+ @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
- MU.log "Initializing an independent instance of #{self.class.name} named #{mu_name}", MU::DEBUG, details: kitten_cfg
- end
- if !kitten_cfg.has_key?("cloud")
- kitten_cfg['cloud'] = MU::Config.defaultCloud
- end
- @cloud = kitten_cfg['cloud']
- @cloudclass = MU::Cloud.loadCloudType(@cloud, self.class.shortname)
- @environment = kitten_cfg['environment']
- @method_semaphore = Mutex.new
- @method_locks = {}
-# XXX require subclass to provide attr_readers of @config and @deploy
- @cloudobj = @cloudclass.new(mommacat: mommacat, kitten_cfg: kitten_cfg, cloud_id: cloud_id, mu_name: mu_name)
+ # Declare the attributes that everyone should have
+ class << self
+ PUBLIC_ATTRS.each { |a|
+ attr_reader a
+ }
+ end
- raise MuError, "Unknown error instantiating #{self}" if @cloudobj.nil?
+# 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 we just loaded an existing object, go ahead and prepopulate the
-# describe() cache
- if !cloud_id.nil? or !mu_name.nil?
- @cloudobj.describe(cloud_id: cloud_id)
- @cloud_id ||= @cloudobj.cloud_id
- end
+ 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
- @deploydata = @cloudobj.deploydata
- @config = @cloudobj.config
+ @deploy = args[:mommacat] || args[:deploy]
-# If we're going to be integrated into AD or otherwise need a short
-# hostname, generate it now.
- if self.class.shortname == "Server" and (@cloudobj.windows? or @config['active_directory']) and @cloudobj.mu_windows_name.nil?
- if !@deploydata.nil? and !@deploydata['mu_windows_name'].nil?
- @cloudobj.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 = @cloudobj.mu_name.sub(/^.*?-(...)$/, '\1')
- if @config['basis'] and !unq.nil? and !unq.empty?
- @cloudobj.mu_windows_name = @deploy.getResourceName(@config['name'], max_length: 15, need_unique_string: true, use_unique_string: unq, reuse_unique_string: true)
- else
- @cloudobj.mu_windows_name = @deploy.getResourceName(@config['name'], max_length: 15, need_unique_string: true)
+ @credentials = args[:credentials]
+ @credentials ||= @config['credentials']
+
+ @cloud = @config['cloud']
+ if !@cloud
+ if self.class.name.match(/^MU::Cloud::([^:]+)(?:::.+|$)/)
+ cloudclass_name = Regexp.last_match[1]
+ if MU::Cloud.supportedClouds.include?(cloudclass_name)
+ @cloud = cloudclass_name
+ end
end
end
- end
+ if !@cloud
+ raise MuError, "Failed to determine what cloud #{self} should be in!"
+ end
- # Register us with our parent deploy so that we can be found by our
- # littermates if needed.
- if !@deploy.nil? and !@cloudobj.mu_name.nil? and !@cloudobj.mu_name.empty?
- describe # XXX is this actually safe here?
- @deploy.addKitten(self.class.cfg_name, @config['name'], self)
- elsif !@deploy.nil?
- MU.log "#{self} didn't generate a mu_name after being loaded/initialized, dependencies on this resource will probably be confused!", MU::ERR
+ @environment = @config['environment']
+ if @deploy
+ @deploy_id = @deploy.deploy_id
+ @appname = @deploy.appname
+ end
+
+ @cloudclass = MU::Cloud.loadCloudType(@cloud, self.class.shortname)
+ @cloudparentclass = Object.const_get("MU").const_get("Cloud").const_get(@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 = Object.const_get("MU").const_get("Cloud").const_get(@cloud).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
+
+ 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
end
end
+ def cloud
+ if @cloud
+ @cloud
+ elsif @config and @config['cloud']
+ @config['cloud']
+ elsif self.class.name.match(/^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 !@cloudobj.nil? and !@cloudobj.groomer.nil?
@cloudobj.groomer.cleanup
elsif !@groomer.nil?
@@ -794,28 +1166,104 @@
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.to_s} FROM PARENT CLOUD OBJECT #{self}", MU::DEBUG, details: arguments
+ @cloudobj.method(method_sym).call(*arguments)
+ else
+ raise NoMethodError, "No such instance method #{method_sym.to_s} 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()
+ def cloud_desc(use_cache: true)
describe
+
if !@cloudobj.nil?
- @cloud_desc_cache ||= @cloudobj.cloud_desc
- @url = @cloudobj.url if @cloudobj.respond_to?(:url)
+ 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 @cloud_desc_cache.nil?
+ 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(region: @config['region'], cloud_id: @cloud_id, flags: @config, credentials: @credentials)
- if !matches.nil? and matches.is_a?(Hash) and matches.has_key?(@cloud_id)
- @cloud_desc_cache = matches[@cloud_id]
- else
- MU.log "Failed to find a live #{self.class.shortname} with identifier #{@cloud_id} in #{@credentials}/#{@config['region']}, which has a record in deploy #{@deploy.deploy_id}", MU::WARN, details: caller
+ 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[0]}", MU::WARN, details: args
+ end
rescue Exception => 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
@@ -856,23 +1304,10 @@
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']
- else
- # XXX temp hack to catch old Amazon-style identifiers. Remove this
- # before supporting any other cloud layers, otherwise name
- # collision is possible.
- ["group_id", "instance_id", "awsname", "identifier", "vpc_id", "id"].each { |identifier|
- if @deploydata.has_key?(identifier)
- @cloud_id ||= @deploydata[identifier]
- if @mu_name.nil? and (identifier == "awsname" or identifier == "identifier" or identifier == "group_id")
- @mu_name = @deploydata[identifier]
- end
- break
- end
- }
end
end
return [@mu_name, @config, @deploydata]
end
@@ -884,94 +1319,134 @@
# 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)
+ def dependencies(use_cache: false, debug: false)
@dependencies = {} if @dependencies.nil?
@loadbalancers = [] if @loadbalancers.nil?
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}", MU::DEBUG
+ 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?
- MU.log "Loading VPC for #{self}", MU::DEBUG, details: @config['vpc']
- if !@config['vpc']["vpc_name"].nil? and @deploy
- sib_by_name = @deploy.findLitterMate(name: @config['vpc']['vpc_name'], type: "vpcs", return_all: true)
+ # 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? and @config['vpc']["id"].is_a?(MU::Config::Ref) and !@config['vpc']["id"].kitten.nil?
+ @vpc = @config['vpc']["id"].kitten
+ 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?(Array)
if sib_by_name.size == 1
@vpc = matches.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.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
- else
- MU.log "Got multiple matching VPCs for #{@mu_name}, so I'm arbitrarily choosing #{sibling.mu_name}"
+ 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.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']["vpc_name"].nil? and
+ if !@vpc and !@config['vpc']["name"].nil? and
@dependencies.has_key?("vpc") and
- @dependencies["vpc"].has_key?(@config['vpc']["vpc_name"])
- @vpc = @dependencies["vpc"][@config['vpc']["vpc_name"]]
+ @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?("vpc_id") and
+ 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']["vpc_id"],
- name: @config['vpc']["vpc_name"],
+ 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,
- dummy_ok: true
+ credentials: @credentials,
+ dummy_ok: true,
+ debug: debug
)
@vpc = vpcs.first if !vpcs.nil? and vpcs.size > 0
end
- if !@vpc.nil? and (
+ 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")
)
@@ -1006,10 +1481,26 @@
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.to_s}", 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|
@@ -1044,21 +1535,50 @@
# ALPHA. That'll learn 'em.
def self.quality
MU::Cloud::ALPHA
end
+ # Return a list of "container" artifacts, by class, that apply to this
+ # resource type in a cloud provider. This is so methods that call find
+ # know whether to call +find+ with identifiers for parent resources.
+ # This is similar in purpose to the +isGlobal?+ resource class method,
+ # which tells our search functions whether or not a resource scopes to
+ # a region. In almost all cases this is one-entry list consisting of
+ # +:Habitat+. Notable exceptions include most implementations of
+ # +Habitat+, which either reside inside a +:Folder+ or nothing at all;
+ # whereas a +:Folder+ tends to not have any containing parent. Very few
+ # resource implementations will need to override this.
+ # A +nil+ entry in this list is interpreted as "this resource can be
+ # global."
+ # @return [Array<Symbol,nil>]
+ def self.canLiveIn
+ if self.shortname == "Folder"
+ [nil, :Folder]
+ elsif self.shortname == "Habitat"
+ [:Folder]
+ else
+ [:Habitat]
+ end
+ end
+
def self.find(*flags)
allfound = {}
- MU::Cloud.supportedClouds.each { |cloud|
+ MU::Cloud.availableClouds.each { |cloud|
begin
args = flags.first
+ next if args[:cloud] and args[:cloud] != cloud
# skip this cloud if we have a region argument that makes no
# sense there
cloudbase = Object.const_get("MU").const_get("Cloud").const_get(cloud)
+ next if cloudbase.listCredentials.nil? or cloudbase.listCredentials.empty? or cloudbase.credConfig(args[:credentials]).nil?
if args[:region] and cloudbase.respond_to?(:listRegions)
- next if !cloudbase.listRegions(credentials: args[:credentials]).include?(args[:region])
+ if !cloudbase.listRegions(credentials: args[:credentials])
+ MU.log "Failed to get region list for credentials #{args[:credentials]} in cloud #{cloud}", MU::ERR, details: caller
+ else
+ next if !cloudbase.listRegions(credentials: args[:credentials]).include?(args[:region])
+ end
end
begin
cloudclass = MU::Cloud.loadCloudType(cloud, shortname)
rescue MU::MuError => e
next
@@ -1092,13 +1612,13 @@
cloudclass.createRecordsFromConfig(*flags)
end
end
end
- if shortname == "Server"
+ if shortname == "Server" or shortname == "ServerPool"
def windows?
- return true if %w{win2k16 win2k12r2 win2k12 win2k8 win2k8r2 windows}.include?(@config['platform'])
+ return true if %w{win2k16 win2k12r2 win2k12 win2k8 win2k8r2 win2k19 windows}.include?(@config['platform'])
begin
return true if cloud_desc.respond_to?(:platform) and cloud_desc.platform == "Windows"
# XXX ^ that's AWS-speak, doesn't cover GCP or anything else; maybe we should require cloud layers to implement this so we can just call @cloudobj.windows?
rescue MU::MuError
return false
@@ -1328,11 +1848,13 @@
else
output = ssh.exec!(lnx_installer_check)
if !output.nil? and !output.empty?
raise MU::Cloud::BootstrapTempFail, "Linux package manager is still doing something, need to wait (#{output})"
end
- if !@config['skipinitialupdates']
+ if !@config['skipinitialupdates'] and
+ !@config['scrub_mu_isms'] and
+ !@config['userdata_script']
output = ssh.exec!(lnx_updates_check)
if !output.nil? and output.match(/userdata still running/)
raise MU::Cloud::BootstrapTempFail, "Waiting for initial userdata system updates to complete"
end
end
@@ -1462,20 +1984,20 @@
sleep 10
end
if retries < max_retries
retries = retries + 1
- msg = "ssh #{ssh_user}@#{@config['mu_name']}: #{e.message}, waiting #{retry_interval}s (attempt #{retries}/#{max_retries})", MU::WARN
+ msg = "ssh #{ssh_user}@#{@mu_name}: #{e.message}, waiting #{retry_interval}s (attempt #{retries}/#{max_retries})", MU::WARN
if retries == 1 or (retries/max_retries <= 0.5 and (retries % 3) == 0)
MU.log msg, MU::NOTICE
elsif retries/max_retries > 0.5
MU.log msg, MU::WARN, details: e.inspect
end
sleep retry_interval
retry
else
- raise MuError, "#{@config['mu_name']}: #{e.inspect} trying to connect with SSH, max_retries exceeded", e.backtrace
+ raise MuError, "#{@mu_name}: #{e.inspect} trying to connect with SSH, max_retries exceeded", e.backtrace
end
end
return session
end
end
@@ -1499,10 +2021,21 @@
end
}
MU::MommaCat.unlockAll
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
+
# 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?
@@ -1536,18 +2069,22 @@
retval = @cloudobj.method(method).call
end
if (method == :create or method == :groom or method == :postBoot) 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", MU::WARN
+ 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['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
- retval['cloud_id'] = @cloudobj.cloud_id if !@cloudobj.cloud_id.nil?
+ 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
@method_semaphore.synchronize {
@method_locks.delete(method)