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