modules/mu/clouds/aws.rb in cloud-mu-2.1.0beta vs modules/mu/clouds/aws.rb in cloud-mu-3.0.0beta
- old
+ new
@@ -26,10 +26,28 @@
class AWS
@@myRegion_var = nil
@@creds_loaded = {}
+ # Module used by {MU::Cloud} to insert additional instance methods into
+ # instantiated resources in this cloud layer.
+ module AdditionalResourceMethods
+ end
+
+ # A hook that is always called just before any of the instance method of
+ # our resource implementations gets invoked, so that we can ensure that
+ # repetitive setup tasks (like resolving +:resource_group+ for Azure
+ # resources) have always been done.
+ # @param cloudobj [MU::Cloud]
+ # @param deploy [MU::MommaCat]
+ def self.resourceInitHook(cloudobj, deploy)
+ class << self
+ attr_reader :cloudformation_data
+ end
+ cloudobj.instance_variable_set(:@cloudformation_data, {})
+ end
+
# Load some credentials for using the AWS API
# @param name [String]: The name of the mu.yaml AWS credential set to use. If not specified, will use the default credentials, and set the global Aws.config credentials to those.
# @return [Aws::Credentials]
def self.loadCredentials(name = nil)
@@creds_loaded ||= {}
@@ -108,11 +126,11 @@
# }
end
else
MU.log "AWS credentials vault:item #{cred_cfg["credentials"]} specified, but is missing access_key or access_secret elements", MU::WARN
end
- rescue MU::Groomer::Chef::MuNoSuchSecret
+ rescue MU::Groomer::MuNoSuchSecret
MU.log "AWS credentials vault:item #{cred_cfg["credentials"]} specified, but does not exist", MU::WARN
end
end
if !cred_obj and hosted?
@@ -143,43 +161,103 @@
end
# Given an AWS region, check the API to make sure it's a valid one
# @param r [String]
# @return [String]
- def self.validate_region(r)
- MU::Cloud::AWS.ec2(region: r).describe_availability_zones.availability_zones.first.region_name
+ def self.validate_region(r, credentials: nil)
+ begin
+ MU::Cloud::AWS.ec2(region: r, credentials: credentials).describe_availability_zones.availability_zones.first.region_name
+ rescue ::Aws::EC2::Errors::UnauthorizedOperation => e
+ MU.log "Got '#{e.message}' trying to validate region #{r} (hosted: #{hosted?.to_s})", MU::ERR, details: loadCredentials(credentials)
+ raise MuError, "Got '#{e.message}' trying to validate region #{r} with credentials #{credentials ? credentials : "<default>"} (hosted: #{hosted?.to_s})"
+ end
end
+ # Tag a resource with all of our standard identifying tags.
+ #
+ # @param resource [String]: The cloud provider identifier of the resource to tag
+ # @param region [String]: The cloud provider region
+ # @return [void]
+ def self.createStandardTags(resource = nil, region: MU.curRegion, credentials: nil)
+ tags = []
+ MU::MommaCat.listStandardTags.each_pair { |name, value|
+ if !value.nil?
+ tags << {key: name, value: value}
+ end
+ }
+ if MU::Cloud::CloudFormation.emitCloudFormation
+ return tags
+ end
+
+ attempts = 0
+ begin
+ MU::Cloud::AWS.ec2(region: region, credentials: credentials).create_tags(
+ resources: [resource],
+ tags: tags
+ )
+ rescue Aws::EC2::Errors::ServiceError => e
+ MU.log "Got #{e.inspect} tagging #{resource} in #{region}, will retry", MU::WARN, details: caller.concat(tags) if attempts > 1
+ if attempts < 5
+ attempts = attempts + 1
+ sleep 15
+ retry
+ else
+ raise e
+ end
+ end
+ MU.log "Created standard tags for resource #{resource}", MU::DEBUG, details: caller
+ end
+
+ # If we reside in this cloud, return the VPC in which we, the Mu Master, reside.
+ # @return [MU::Cloud::VPC]
+ def self.myVPCObj
+ return nil if !hosted?
+ instance = MU.myCloudDescriptor
+ return nil if !instance or !instance.vpc_id
+ vpc = MU::MommaCat.findStray("AWS", "vpc", cloud_id: instance.vpc_id, dummy_ok: true)
+ return nil if vpc.nil? or vpc.size == 0
+ vpc.first
+ end
+
# If we've configured AWS as a provider, or are simply hosted in AWS,
# decide what our default region is.
- def self.myRegion
+ def self.myRegion(credentials = nil)
return @@myRegion_var if @@myRegion_var
if credConfig.nil? and !hosted? and !ENV['EC2_REGION']
return nil
end
-
if $MU_CFG and $MU_CFG['aws']
$MU_CFG['aws'].each_pair { |credset, cfg|
+ next if credentials and credset != credentials
next if !cfg['region']
- if (cfg['default'] or !@@myRegion_var) and validate_region(cfg['region'])
+ if (cfg['default'] or !@@myRegion_var) and validate_region(cfg['region'], credentials: credset)
@@myRegion_var = cfg['region']
- break if cfg['default']
+ break if cfg['default'] or credentials
end
}
elsif ENV.has_key?("EC2_REGION") and !ENV['EC2_REGION'].empty? and
- validate_region(ENV['EC2_REGION'])
+ validate_region(ENV['EC2_REGION']) and
+ (
+ (ENV.has_key?("AWS_SECRET_ACCESS_KEY") and ENV.has_key?("AWS_SECRET_ACCESS_KEY") ) or
+ (Aws.config['access_key'] and Aws.config['access_secret'])
+ )
# Make sure this string is valid by way of the API
@@myRegion_var = ENV['EC2_REGION']
- else
+ end
+
+ if hosted? and !@@myRegion_var
# hacky, but useful in a pinch (and if we're hosted in AWS)
az_str = MU::Cloud::AWS.getAWSMetaData("placement/availability-zone")
@@myRegion_var = az_str.sub(/[a-z]$/i, "") if az_str
end
+
+ @@myRegion_var
end
+
# Is the region we're dealing with a GovCloud region?
# @param region [String]: The region in question, defaults to the Mu Master's local region
def self.isGovCloud?(region = myRegion)
return false if !region
region.match(/^us-gov-/)
@@ -245,13 +323,12 @@
# region. If no region is given, search the one in which this MU master
# server resides.
# @param region [String]: The region to search.
# @return [Array<String>]: The Availability Zones in this region.
def self.listAZs(region: MU.curRegion, account: nil, credentials: nil)
- if $MU_CFG and (!$MU_CFG['aws'] or !account_number)
- return []
- end
+ cfg = credConfig(credentials)
+ return [] if !cfg
if !region.nil? and @@azs[region]
return @@azs[region]
end
if region
azs = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_availability_zones(
@@ -263,10 +340,22 @@
@@azs[region] << az.zone_name if az.state == "available"
}
return @@azs[region]
end
+ # Do cloud-specific deploy instantiation tasks, such as copying SSH keys
+ # around, sticking secrets in buckets, creating resource groups, etc
+ # @param deploy [MU::MommaCat]
+ def self.initDeploy(deploy)
+ end
+
+ # Purge cloud-specific deploy meta-artifacts (SSH keys, resource groups,
+ # etc)
+ # @param deploy_id [MU::MommaCat]
+ def self.cleanDeploy(deploy_id, credentials: nil, noop: false)
+ end
+
# Plant a Mu deploy secret into a storage bucket somewhere for so our kittens can consume it
# @param deploy_id [String]: The deploy for which we're writing the secret
# @param value [String]: The contents of the secret
def self.writeDeploySecret(deploy_id, value, name = nil, credentials: nil)
name ||= deploy_id+"-secret"
@@ -344,10 +433,18 @@
begin
Timeout.timeout(2) do
instance_id = open("http://169.254.169.254/latest/meta-data/instance-id").read
if !instance_id.nil? and instance_id.size > 0
@@is_in_aws = true
+ region = getAWSMetaData("placement/availability-zone").sub(/[a-z]$/i, "")
+ begin
+ validate_region(region)
+ rescue MuError
+ @@creds_loaded.delete("#default")
+ @@is_in_aws = false
+ false
+ end
return true
end
end
rescue OpenURI::HTTPError, Timeout::Error, SocketError, Errno::EHOSTUNREACH
end
@@ -382,10 +479,20 @@
sample["credentials_file"] = "#{Etc.getpwuid(Process.uid).dir}/.aws/credentials"
sample["log_bucket_name"] = "my-mu-s3-bucket"
sample
end
+ # Return what we think of as a cloud object's habitat. In AWS, this means
+ # the +account_number+ in which it's resident. If this is not applicable,
+ # such as for a {Habitat} or {Folder}, returns nil.
+ # @param cloudobj [MU::Cloud::AWS]: The resource from which to extract the habitat id
+ # @return [String,nil]
+ def self.habitat(cloudobj, nolookup: false, deploy: nil)
+ cloudobj.respond_to?(:account_number) ? cloudobj.account_number : nil
+ end
+
+
@@my_acct_num = nil
@@my_hosted_cfg = nil
@@acct_to_profile_map = {}
# Map the name of a credential set back to an AWS account number
@@ -413,21 +520,43 @@
# Resolve the administrative S3 bucket for a given credential set, or
# return a default.
# @param credentials [String]
# @return [String]
def self.adminBucketName(credentials = nil)
- #XXX find a default if this particular account doesn't have a log_bucket_name configured
cfg = credConfig(credentials)
+ return nil if !cfg
+ if !cfg['log_bucket_name']
+ cfg['log_bucket_name'] = $MU_CFG['hostname']
+ MU.log "No AWS log bucket defined for credentials #{credentials}, attempting to use default of #{cfg['log_bucket_name']}", MU::WARN
+ end
+ resp = MU::Cloud::AWS.s3(credentials: credentials).list_buckets
+ found = false
+ resp.buckets.each { |b|
+ if b.name == cfg['log_bucket_name']
+ found = true
+ break
+ end
+ }
+ if !found
+ MU.log "Attempting to create log bucket #{cfg['log_bucket_name']} for credentials #{credentials}", MU::WARN
+ begin
+ resp = MU::Cloud::AWS.s3(credentials: credentials).create_bucket(bucket: cfg['log_bucket_name'], acl: "private")
+ rescue Aws::S3::Errors::BucketAlreadyExists => e
+ raise MuError, "AWS credentials #{credentials} need a log bucket, and the name #{cfg['log_bucket_name']} is unavailable. Use mu-configure to edit credentials '#{credentials}' or 'hostname'"
+ end
+ end
+
cfg['log_bucket_name']
end
# Resolve the administrative S3 bucket for a given credential set, or
# return a default.
# @param credentials [String]
# @return [String]
def self.adminBucketUrl(credentials = nil)
- "s3://"+adminBucketName+"/"
+ return nil if !credConfig(credentials)
+ "s3://"+adminBucketName(credentials)+"/"
end
# Return the $MU_CFG data associated with a particular profile/name/set of
# credentials. If no account name is specified, will return one flagged as
# default. Returns nil if AWS is not configured. Throws an exception if
@@ -437,11 +566,13 @@
def self.credConfig(name = nil, name_only: false)
# If there's nothing in mu.yaml (which is wrong), but we're running
# on a machine hosted in AWS, *and* that machine has an IAM profile,
# fake it with those credentials and hope for the best.
if !$MU_CFG['aws'] or !$MU_CFG['aws'].is_a?(Hash) or $MU_CFG['aws'].size == 0
- return @@my_hosted_cfg if @@my_hosted_cfg
+ if @@my_hosted_cfg
+ return name_only ? "#default" : @@my_hosted_cfg
+ end
if hosted?
begin
iam_data = JSON.parse(getAWSMetaData("iam/info"))
if iam_data["InstanceProfileArn"] and !iam_data["InstanceProfileArn"].empty?
@@ -462,13 +593,13 @@
return nil
end
if name.nil?
- $MU_CFG['aws'].each_pair { |name, cfg|
+ $MU_CFG['aws'].each_pair { |set, cfg|
if cfg['default']
- return name_only ? name : cfg
+ return name_only ? set : cfg
end
}
else
if $MU_CFG['aws'][name]
return name_only ? name : $MU_CFG['aws'][name]
@@ -494,11 +625,10 @@
acct_num = MU::Cloud::AWS.iam(credentials: acctname).list_users.users.first.arn.split(/:/)[4]
if acct_num.to_s == name.to_s
cfg['account_number'] = acct_num.to_s
@@acct_to_profile_map[name.to_s] = cfg
return name_only ? name.to_s : cfg
- return cfg
end
}
end
raise MuError, "AWS credential set #{name} was requested, but I see no such working credentials in mu.yaml"
@@ -1024,14 +1154,18 @@
# Punch AWS security group holes for client nodes to talk back to us, the
# Mu Master, if we're in AWS.
# @return [void]
def self.openFirewallForClients
MU::Cloud.loadCloudType("AWS", :FirewallRule)
- if File.exists?(Etc.getpwuid(Process.uid).dir+"/.chef/knife.rb")
- ::Chef::Config.from_file(Etc.getpwuid(Process.uid).dir+"/.chef/knife.rb")
+ begin
+ if File.exist?(Etc.getpwuid(Process.uid).dir+"/.chef/knife.rb")
+ ::Chef::Config.from_file(Etc.getpwuid(Process.uid).dir+"/.chef/knife.rb")
+ end
+ ::Chef::Config[:environment] = MU.environment
+ rescue LoadError
+ # XXX why is Chef here
end
- ::Chef::Config[:environment] = MU.environment
# This is the set of (TCP) ports we're opening to clients. We assume that
# we can and and remove these without impacting anything a human has
# created.
@@ -1216,12 +1350,10 @@
params[:credentials] = @cred_obj
MU.log "Initializing #{api} object with credentials #{credentials}", MU::DEBUG, details: params
@api = Object.const_get("Aws::#{api}::Client").new(params)
-
- @api
end
@instance_cache = {}
# Catch-all for AWS client methods. Essentially a pass-through with some
# rescues for known silly endpoint behavior.
@@ -1237,10 +1369,10 @@
retval = @api.method(method_sym).call(*arguments)
else
retval = @api.method(method_sym).call
end
return retval
- rescue Aws::EC2::Errors::InternalError, Aws::EC2::Errors::RequestLimitExceeded, Aws::EC2::Errors::Unavailable, Aws::Route53::Errors::Throttling, Aws::ElasticLoadBalancing::Errors::HttpFailureException, Aws::EC2::Errors::Http503Error, Aws::AutoScaling::Errors::Http503Error, Aws::AutoScaling::Errors::InternalFailure, Aws::AutoScaling::Errors::ServiceUnavailable, Aws::Route53::Errors::ServiceUnavailable, Aws::ElasticLoadBalancing::Errors::Throttling, Aws::RDS::Errors::ClientUnavailable, Aws::Waiters::Errors::UnexpectedError, Aws::ElasticLoadBalancing::Errors::ServiceUnavailable, Aws::ElasticLoadBalancingV2::Errors::Throttling, Seahorse::Client::NetworkingError, Aws::IAM::Errors::Throttling, Aws::EFS::Errors::ThrottlingException, Aws::Pricing::Errors::ThrottlingException, Aws::APIGateway::Errors::TooManyRequestsException, Aws::ECS::Errors::ThrottlingException => e
+ rescue Aws::EC2::Errors::InternalError, Aws::EC2::Errors::RequestLimitExceeded, Aws::EC2::Errors::Unavailable, Aws::Route53::Errors::Throttling, Aws::ElasticLoadBalancing::Errors::HttpFailureException, Aws::EC2::Errors::Http503Error, Aws::AutoScaling::Errors::Http503Error, Aws::AutoScaling::Errors::InternalFailure, Aws::AutoScaling::Errors::ServiceUnavailable, Aws::Route53::Errors::ServiceUnavailable, Aws::ElasticLoadBalancing::Errors::Throttling, Aws::RDS::Errors::ClientUnavailable, Aws::Waiters::Errors::UnexpectedError, Aws::ElasticLoadBalancing::Errors::ServiceUnavailable, Aws::ElasticLoadBalancingV2::Errors::Throttling, Seahorse::Client::NetworkingError, Aws::IAM::Errors::Throttling, Aws::EFS::Errors::ThrottlingException, Aws::Pricing::Errors::ThrottlingException, Aws::APIGateway::Errors::TooManyRequestsException, Aws::ECS::Errors::ThrottlingException, Net::ReadTimeout, Faraday::TimeoutError => e
if e.class.name == "Seahorse::Client::NetworkingError" and e.message.match(/Name or service not known/)
MU.log e.inspect, MU::ERR
raise e
end
retries = retries + 1