modules/mu/config.rb in cloud-mu-3.1.6 vs modules/mu/config.rb in cloud-mu-3.2.0
- old
+ new
@@ -428,10 +428,43 @@
#pp @config
#raise "DERP"
@config.freeze
end
+ # Insert a dependency into the config hash of a resource, with sensible
+ # error checking and de-duplication.
+ # @param resource [Hash]
+ # @param name [String]
+ # @param type [String]
+ # @param phase [String]
+ # @param no_create_wait [Boolean]
+ def self.addDependency(resource, name, type, phase: nil, no_create_wait: false)
+ if ![nil, "create", "groom"].include?(phase)
+ raise MuError, "Invalid phase '#{phase}' while adding dependency #{type} #{name} to #{resource['name']}"
+ end
+ resource['dependencies'] ||= []
+ _shortclass, cfg_name, _cfg_plural, _classname = MU::Cloud.getResourceNames(type)
+
+ resource['dependencies'].each { |dep|
+ if dep['type'] == cfg_name and dep['name'].to_s == name.to_s
+ dep["no_create_wait"] = no_create_wait
+ dep["phase"] = phase if phase
+ return
+ end
+ }
+
+ newdep = {
+ "type" => cfg_name,
+ "name" => name.to_s,
+ "no_create_wait" => no_create_wait
+ }
+ newdep["phase"] = phase if phase
+
+ resource['dependencies'] << newdep
+
+ end
+
# See if a given resource is configured in the current stack
# @param name [String]: The name of the resource being checked
# @param type [String]: The type of resource being checked
# @return [Boolean]
def haveLitterMate?(name, type, has_multiple: false)
@@ -483,11 +516,12 @@
# @param type [String]: The type of resource being added
# @param delay_validation [Boolean]: Whether to hold off on calling the resource's validateConfig method
# @param ignore_duplicates [Boolean]: Do not raise an exception if we attempt to insert a resource with a +name+ field that's already in use
def insertKitten(descriptor, type, delay_validation = false, ignore_duplicates: false, overwrite: false)
append = false
-# start = Time.now
+ start = Time.now
+
shortclass, cfg_name, cfg_plural, classname = MU::Cloud.getResourceNames(type)
MU.log "insertKitten on #{cfg_name} #{descriptor['name']} (delay_validation: #{delay_validation.to_s})", MU::DEBUG, details: caller[0]
if overwrite
removeKitten(descriptor['name'], type)
@@ -523,11 +557,11 @@
# Meld defaults from our global schema and, if applicable, from our
# cloud-specific schema.
schemaclass = Object.const_get("MU").const_get("Config").const_get(shortclass)
myschema = Marshal.load(Marshal.dump(MU::Config.schema["properties"][cfg_plural]["items"]))
- more_required, more_schema = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get(shortclass.to_s).schema(self)
+ more_required, more_schema = MU::Cloud.resourceClass(descriptor["cloud"], type).schema(self)
if more_schema
MU::Config.schemaMerge(myschema["properties"], more_schema, descriptor["cloud"])
end
myschema["required"] ||= []
if more_required
@@ -542,11 +576,11 @@
(descriptor['cloud'] == "Google" and ["firewall_rule", "vpc"].include?(cfg_name))
descriptor.delete("region")
end
# Make sure a sensible region has been targeted, if applicable
- classobj = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"])
+ classobj = MU::Cloud.cloudClass(descriptor["cloud"])
if descriptor["region"]
valid_regions = classobj.listRegions
if !valid_regions.include?(descriptor["region"])
MU.log "Known regions for cloud '#{descriptor['cloud']}' do not include '#{descriptor["region"]}'", MU::ERR, details: valid_regions
ok = false
@@ -555,15 +589,11 @@
if descriptor.has_key?('project')
if descriptor['project'].nil?
descriptor.delete('project')
elsif haveLitterMate?(descriptor['project'], "habitats")
- descriptor['dependencies'] ||= []
- descriptor['dependencies'] << {
- "type" => "habitat",
- "name" => descriptor['project']
- }
+ MU::Config.addDependency(descriptor, descriptor['project'], "habitat")
end
end
# Does this resource go in a VPC?
if !descriptor["vpc"].nil? and !delay_validation
@@ -587,25 +617,20 @@
# If we're using a VPC in this deploy, set it as a dependency
if !descriptor["vpc"]["name"].nil? and
haveLitterMate?(descriptor["vpc"]["name"], "vpcs") and
descriptor["vpc"]['deploy_id'].nil? and
- descriptor["vpc"]['id'].nil?
- descriptor["dependencies"] << {
- "type" => "vpc",
- "name" => descriptor["vpc"]["name"],
- }
+ descriptor["vpc"]['id'].nil? and
+ !(cfg_name == "vpc" and descriptor['name'] == descriptor['vpc']['name'])
+ MU::Config.addDependency(descriptor, descriptor['vpc']['name'], "vpc")
siblingvpc = haveLitterMate?(descriptor["vpc"]["name"], "vpcs")
if siblingvpc and siblingvpc['bastion'] and
["server", "server_pool", "container_cluster"].include?(cfg_name) and
!descriptor['bastion']
- if descriptor['name'] != siblingvpc['bastion'].to_h['name']
- descriptor["dependencies"] << {
- "type" => "server",
- "name" => siblingvpc['bastion'].to_h['name']
- }
+ if descriptor['name'] != siblingvpc['bastion']['name']
+ MU::Config.addDependency(descriptor, siblingvpc['bastion']['name'], "server")
end
end
# things that live in subnets need their VPCs to be fully
# resolved before we can proceed
@@ -663,22 +688,21 @@
fwname = cfg_name+descriptor['name']
if (descriptor['ingress_rules'] or
["server", "server_pool", "database", "cache_cluster"].include?(cfg_name))
descriptor['ingress_rules'] ||= []
- fw_classobj = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get("FirewallRule")
acl = haveLitterMate?(fwname, "firewall_rules")
already_exists = !acl.nil?
acl ||= {
"name" => fwname,
"rules" => descriptor['ingress_rules'],
"region" => descriptor['region'],
"credentials" => descriptor["credentials"]
}
- if !fw_classobj.isGlobal?
+ if !MU::Cloud.resourceClass(descriptor["cloud"], "FirewallRule").isGlobal?
acl['region'] = descriptor['region']
acl['region'] ||= classobj.myRegion(acl['credentials'])
else
acl.delete("region")
end
@@ -700,40 +724,31 @@
# Does it declare association with any sibling LoadBalancers?
if !descriptor["loadbalancers"].nil?
descriptor["loadbalancers"].each { |lb|
if !lb["concurrent_load_balancer"].nil?
- descriptor["dependencies"] << {
- "type" => "loadbalancer",
- "name" => lb["concurrent_load_balancer"]
- }
+ MU::Config.addDependency(descriptor, lb["concurrent_load_balancer"], "loadbalancer")
end
}
end
# Does it want to know about Storage Pools?
if !descriptor["storage_pools"].nil?
descriptor["storage_pools"].each { |sp|
if sp["name"]
- descriptor["dependencies"] << {
- "type" => "storage_pool",
- "name" => sp["name"]
- }
+ MU::Config.addDependency(descriptor, sp["name"], "storage_pool")
end
}
end
# Does it declare association with first-class firewall_rules?
if !descriptor["add_firewall_rules"].nil?
descriptor["add_firewall_rules"].each { |acl_include|
next if !acl_include["name"] and !acl_include["rule_name"]
acl_include["name"] ||= acl_include["rule_name"]
if haveLitterMate?(acl_include["name"], "firewall_rules")
- descriptor["dependencies"] << {
- "type" => "firewall_rule",
- "name" => acl_include["name"]
- }
+ MU::Config.addDependency(descriptor, acl_include["name"], "firewall_rule", no_create_wait: (cfg_name == "vpc"))
elsif acl_include["name"]
MU.log shortclass.to_s+" #{descriptor['name']} depends on FirewallRule #{acl_include["name"]}, but no such rule declared.", MU::ERR
ok = false
end
}
@@ -829,21 +844,21 @@
end
# Run the cloud class's deeper validation, unless we've already failed
# on stuff that will cause spurious alarms further in
if ok
- parser = Object.const_get("MU").const_get("Cloud").const_get(descriptor["cloud"]).const_get(shortclass.to_s)
+ parser = MU::Cloud.resourceClass(descriptor['cloud'], type)
original_descriptor = MU::Config.stripConfig(descriptor)
passed = parser.validateConfig(descriptor, self)
if !passed
descriptor = original_descriptor
ok = false
end
# Make sure we've been configured with the right credentials
- cloudbase = Object.const_get("MU").const_get("Cloud").const_get(descriptor['cloud'])
+ cloudbase = MU::Cloud.cloudClass(descriptor['cloud'])
credcfg = cloudbase.credConfig(descriptor['credentials'])
if !credcfg or credcfg.empty?
raise ValidationError, "#{descriptor['cloud']} #{cfg_name} #{descriptor['name']} declares credential set #{descriptor['credentials']}, but no such credentials exist for that cloud provider"
end
@@ -855,64 +870,96 @@
@kittencfg_semaphore.synchronize {
@kittens[cfg_plural] << descriptor if append
}
+ MU.log "insertKitten completed #{cfg_name} #{descriptor['name']} in #{sprintf("%.2fs", Time.now-start)}", MU::DEBUG
+
ok
end
# For our resources which specify intra-stack dependencies, make sure those
# dependencies are actually declared.
- # TODO check for loops
- def self.check_dependencies(config)
+ def check_dependencies
ok = true
- config.each_pair { |type, values|
- if values.instance_of?(Array)
- values.each { |resource|
- if resource.kind_of?(Hash) and !resource["dependencies"].nil?
- append = []
- delete = []
- resource["dependencies"].each { |dependency|
- _shortclass, cfg_name, cfg_plural, _classname = MU::Cloud.getResourceNames(dependency["type"])
- found = false
- names_seen = []
- if !config[cfg_plural].nil?
- config[cfg_plural].each { |service|
- names_seen << service["name"].to_s
- found = true if service["name"].to_s == dependency["name"].to_s
- if service["virtual_name"]
- names_seen << service["virtual_name"].to_s
- if service["virtual_name"].to_s == dependency["name"].to_s
- found = true
- append_me = dependency.dup
- append_me['name'] = service['name']
- append << append_me
- delete << dependency
- end
- end
+ @config.each_pair { |type, values|
+ next if !values.instance_of?(Array)
+ _shortclass, cfg_name, _cfg_plural, _classname = MU::Cloud.getResourceNames(type, false)
+ next if !cfg_name
+ values.each { |resource|
+ next if !resource.kind_of?(Hash) or resource["dependencies"].nil?
+ addme = []
+ deleteme = []
+
+ resource["dependencies"].each { |dependency|
+ # make sure the thing we depend on really exists
+ sibling = haveLitterMate?(dependency['name'], dependency['type'])
+ if !sibling
+ MU.log "Missing dependency: #{type}{#{resource['name']}} needs #{cfg_name}{#{dependency['name']}}", MU::ERR
+ ok = false
+ next
+ end
+
+ # Fudge dependency declarations to quash virtual_names that we know
+ # are extraneous. Note that wee can't do all virtual names here; we
+ # have no way to guess which of a collection of resources is the
+ # real correct one.
+ if sibling['virtual_name'] == dependency['name']
+ real_resources = []
+ found_exact = false
+ resource["dependencies"].each { |dep_again|
+ if dep_again['type'] == dependency['type'] and sibling['name'] == dep_again['name']
+ dependency['name'] = sibling['name']
+ found_exact = true
+ break
+ end
+ }
+ if !found_exact
+ all_siblings = haveLitterMate?(dependency['name'], dependency['type'], has_multiple: true)
+ if all_siblings.size > 0
+ all_siblings.each { |s|
+ newguy = dependency.clone
+ newguy['name'] = s['name']
+ addme << newguy
}
+ deleteme << dependency
+ MU.log "Expanding dependency which maps to virtual resources to all matching real resources", MU::NOTICE, details: { sibling['virtual_name'] => addme }
+ next
end
- if !found
- MU.log "Missing dependency: #{type}{#{resource['name']}} needs #{cfg_name}{#{dependency['name']}}", MU::ERR, details: names_seen
+ end
+ end
+
+ # Check for a circular relationship that will lead to a deadlock
+ # when creating resource. This only goes one layer deep, and does
+ # not consider groom-phase deadlocks.
+ if dependency['phase'] == "groom" or dependency['no_create_wait'] or (
+ !MU::Cloud.resourceClass(sibling['cloud'], type).deps_wait_on_my_creation and
+ !MU::Cloud.resourceClass(resource['cloud'], type).waits_on_parent_completion
+ )
+ next
+ end
+
+ if sibling['dependencies']
+ sibling['dependencies'].each { |sib_dep|
+ next if sib_dep['type'] != cfg_name or sib_dep['no_create_wait']
+ cousin = haveLitterMate?(sib_dep['name'], sib_dep['type'])
+ if cousin and cousin['name'] == resource['name']
+ MU.log "Circular dependency between #{type} #{resource['name']} <=> #{dependency['type']} #{dependency['name']}", MU::ERR, details: [ resource['name'] => dependency, sibling['name'] => sib_dep ]
ok = false
end
}
- if append.size > 0
- append.uniq!
- resource["dependencies"].concat(append)
- end
- if delete.size > 0
- delete.each { |delete_me|
- resource["dependencies"].delete(delete_me)
- }
- end
end
}
- end
+ resource["dependencies"].reject! { |dep| deleteme.include?(dep) }
+ resource["dependencies"].concat(addme)
+ resource["dependencies"].uniq!
+
+ }
}
- return ok
+
+ ok
end
# Ugly text-manipulation to recursively resolve some placeholder strings
# we put in for ERB include() directives.
# @param lines [String]
@@ -1189,16 +1236,11 @@
ruleset["rules"] << {
"proto" => "tcp",
"port" => db["port"],
"sgs" => [cfg_name+server['name']]
}
-
- ruleset["dependencies"] << {
- "name" => cfg_name+server['name'],
- "type" => "firewall_rule",
- "no_create_wait" => true
- }
+ MU::Config.addDependency(ruleset, cfg_name+server['name'], "firewall_rule", no_create_wait: true)
end
}
}
}
end
@@ -1212,10 +1254,10 @@
seen << acl['name']
}
types.each { |type|
config[type] = @kittens[type] if @kittens[type].size > 0
}
- ok = false if !MU::Config.check_dependencies(config)
+ ok = false if !check_dependencies
# TODO enforce uniqueness of resource names
raise ValidationError if !ok
# XXX Does commenting this out make sense? Do we want to apply it to top-level