modules/mu/clouds/aws.rb in cloud-mu-1.9.0.pre.beta vs modules/mu/clouds/aws.rb in cloud-mu-2.0.0.pre.alpha

- old
+ new

@@ -24,85 +24,116 @@ class Cloud # Support for Amazon Web Services as a provisioning layer. class AWS @@myRegion_var = nil - @@creds_loaded = false + @@creds_loaded = {} # Load some credentials for using the AWS API - def self.loadCredentials - return if @@creds_loaded - if $MU_CFG and $MU_CFG['aws'] - loaded = false - if $MU_CFG['aws']['access_key'] and $MU_CFG['aws']['access_secret'] and - # access key and secret just sitting in mu.yaml - !$MU_CFG['aws']['access_key'].empty? and - !$MU_CFG['aws']['access_secret'].empty? - Aws.config = { - access_key_id: $MU_CFG['aws']['access_key'], - secret_access_key: $MU_CFG['aws']['access_secret'], - region: $MU_CFG['aws']['region'] - } - loaded = true - elsif $MU_CFG['aws']['credentials_file'] and - !$MU_CFG['aws']['credentials_file'].empty? - # pull access key and secret from an awscli-style credentials file - begin - File.read($MU_CFG["aws"]["credentials_file"]) # make sure it's there - credfile = IniFile.load($MU_CFG["aws"]["credentials_file"]) + # @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 ||= {} - if !credfile.sections or credfile.sections.size == 0 - raise ::IniFile::Error, "No AWS profiles found in #{$MU_CFG["aws"]["credentials_file"]}" + if name.nil? + return @@creds_loaded["#default"] if @@creds_loaded["#default"] + else + return @@creds_loaded[name] if @@creds_loaded[name] + end + + cred_cfg = credConfig(name) + if cred_cfg.nil? + return nil + end + + loaded = false + cred_obj = nil + if cred_cfg['access_key'] and cred_cfg['access_secret'] and + # access key and secret just sitting in mu.yaml + !cred_cfg['access_key'].empty? and + !cred_cfg['access_secret'].empty? + cred_obj = Aws::Credentials.new( + cred_cfg['access_key'], cred_cfg['access_secret'] + ) + if name.nil? +# Aws.config = { +# access_key_id: cred_cfg['access_key'], +# secret_access_key: cred_cfg['access_secret'], +# region: cred_cfg['region'] +# } + end + elsif cred_cfg['credentials_file'] and + !cred_cfg['credentials_file'].empty? + + # pull access key and secret from an awscli-style credentials file + begin + File.read(cred_cfg["credentials_file"]) # make sure it's there + credfile = IniFile.load(cred_cfg["credentials_file"]) + + if !credfile.sections or credfile.sections.size == 0 + raise ::IniFile::Error, "No AWS profiles found in #{cred_cfg["credentials_file"]}" + end + data = credfile.has_section?("default") ? credfile["default"] : credfile[credfile.sections.first] + if data["aws_access_key_id"] and data["aws_secret_access_key"] + cred_obj = Aws::Credentials.new( + data['aws_access_key_id'], data['aws_secret_access_key'] + ) + if name.nil? +# Aws.config = { +# access_key_id: data['aws_access_key_id'], +# secret_access_key: data['aws_secret_access_key'], +# region: cred_cfg['region'] +# } end - data = credfile.has_section?("default") ? credfile["default"] : credfile[credfile.sections.first] - if data["aws_access_key_id"] and data["aws_secret_access_key"] - Aws.config = { - access_key_id: data['aws_access_key_id'], - secret_access_key: data['aws_secret_access_key'], - region: $MU_CFG['aws']['region'] - } - loaded = true - else - MU.log "AWS credentials in #{$MU_CFG["aws"]["credentials_file"]} specified, but is missing aws_access_key_id or aws_secret_access_key elements", MU::WARN - end - rescue IniFile::Error, Errno::ENOENT, Errno::EACCES => e - MU.log "AWS credentials file #{$MU_CFG["aws"]["credentials_file"]} is missing or invalid", MU::WARN, details: e.message + else + MU.log "AWS credentials in #{cred_cfg["credentials_file"]} specified, but is missing aws_access_key_id or aws_secret_access_key elements", MU::WARN end - elsif $MU_CFG['aws']['credentials'] and - !$MU_CFG['aws']['credentials'].empty? - # pull access key and secret from a vault - begin - vault, item = $MU_CFG["aws"]["credentials"].split(/:/) - data = MU::Groomer::Chef.getSecret(vault: vault, item: item).to_h - if data["access_key"] and data["access_secret"] - Aws.config = { - access_key_id: data['access_key'], - secret_access_key: data['access_secret'], - region: $MU_CFG['aws']['region'] - } - loaded = true - else - MU.log "AWS credentials vault:item #{$MU_CFG["aws"]["credentials"]} specified, but is missing access_key or access_secret elements", MU::WARN + rescue IniFile::Error, Errno::ENOENT, Errno::EACCES => e + MU.log "AWS credentials file #{cred_cfg["credentials_file"]} is missing or invalid", MU::WARN, details: e.message + end + elsif cred_cfg['credentials'] and + !cred_cfg['credentials'].empty? + # pull access key and secret from a vault + begin + vault, item = cred_cfg["credentials"].split(/:/) + data = MU::Groomer::Chef.getSecret(vault: vault, item: item).to_h + if data["access_key"] and data["access_secret"] + cred_obj = Aws::Credentials.new( + cred_cfg['access_key'], cred_cfg['access_secret'] + ) + if name.nil? +# Aws.config = { +# access_key_id: data['access_key'], +# secret_access_key: data['access_secret'], +# region: cred_cfg['region'] +# } end - rescue MU::Groomer::Chef::MuNoSuchSecret - MU.log "AWS credentials vault:item #{$MU_CFG["aws"]["credentials"]} specified, but does not exist", MU::WARN + 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 + MU.log "AWS credentials vault:item #{cred_cfg["credentials"]} specified, but does not exist", MU::WARN end + end - if !loaded and hosted? - # assume we've got an IAM profile and hope for the best - ENV.delete('AWS_ACCESS_KEY_ID') - ENV.delete('AWS_SECRET_ACCESS_KEY') - Aws.config = {region: ENV['EC2_REGION']} - loaded = true - end + if !cred_obj and hosted? + # assume we've got an IAM profile and hope for the best + ENV.delete('AWS_ACCESS_KEY_ID') + ENV.delete('AWS_SECRET_ACCESS_KEY') + cred_obj = Aws::InstanceProfileCredentials.new +# if name.nil? +# Aws.config = {region: ENV['EC2_REGION']} +# end + end - @@creds_loaded = loaded - if !@@creds_loaded - raise MuError, "AWS layer is enabled in mu.yaml, but I couldn't find working API credentials anywhere" - end + if name.nil? + @@creds_loaded["#default"] = cred_obj + else + @@creds_loaded[name] = cred_obj end + + cred_obj end # Any cloud-specific instance methods we require our resource # implementations to have, above and beyond the ones specified by # {MU::Cloud} @@ -112,17 +143,21 @@ end # If we've configured AWS as a provider, or are simply hosted in AWS, # decide what our default region is. def self.myRegion + return @@myRegion_var if @@myRegion_var + return nil if credConfig.nil? and !hosted? + if $MU_CFG and (!$MU_CFG['aws'] or !account_number) and !hosted? return nil end + if $MU_CFG and $MU_CFG['aws'] and $MU_CFG['aws']['region'] - @@myRegion_var ||= MU::Cloud::AWS.ec2($MU_CFG['aws']['region']).describe_availability_zones.availability_zones.first.region_name + @@myRegion_var ||= MU::Cloud::AWS.ec2(region: $MU_CFG['aws']['region']).describe_availability_zones.availability_zones.first.region_name elsif ENV.has_key?("EC2_REGION") and !ENV['EC2_REGION'].empty? - @@myRegion_var ||= MU::Cloud::AWS.ec2(ENV['EC2_REGION']).describe_availability_zones.availability_zones.first.region_name + @@myRegion_var ||= MU::Cloud::AWS.ec2(region: ENV['EC2_REGION']).describe_availability_zones.availability_zones.first.region_name else # hacky, but useful in a pinch az_str = MU::Cloud::AWS.getAWSMetaData("placement/availability-zone") @@myRegion_var = az_str.sub(/[a-z]$/i, "") if az_str end @@ -138,11 +173,11 @@ # @param resources [Array<String>]: The cloud provider identifier of the resource to untag # @param key [String]: The name of the tag to remove # @param value [String]: The value of the tag to remove # @param region [String]: The cloud provider region def self.removeTag(key, value, resources = [], region: myRegion) - MU::Cloud::AWS.ec2(region).delete_tags( + MU::Cloud::AWS.ec2(region: region).delete_tags( resources: resources, tags: [ { key: key, value: value @@ -156,15 +191,15 @@ # @param resources [Array<String>]: The cloud provider identifier of the resource to tag # @param key [String]: The name of the tag to create # @param value [String]: The value of the tag # @param region [String]: The cloud provider region # @return [void,<Hash>] - def self.createTag(key, value, resources = [], region: myRegion) + def self.createTag(key, value, resources = [], region: myRegion, credentials: nil) if !MU::Cloud::CloudFormation.emitCloudFormation begin - MU::Cloud::AWS.ec2(region).create_tags( + MU::Cloud::AWS.ec2(region: region, credentials: credentials).create_tags( resources: resources, tags: [ { key: key, value: value @@ -194,19 +229,19 @@ # List the Availability Zones associated with a given Amazon Web Services # 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) + def self.listAZs(region: MU.curRegion, account: nil, credentials: nil) if $MU_CFG and (!$MU_CFG['aws'] or !account_number) return [] end if !region.nil? and @@azs[region] return @@azs[region] end if region - azs = MU::Cloud::AWS.ec2(region).describe_availability_zones( + azs = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_availability_zones( filters: [name: "region-name", values: [region]] ) end @@azs[region] ||= [] azs.data.availability_zones.each { |az| @@ -216,25 +251,61 @@ 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) + def self.writeDeploySecret(deploy_id, value, name = nil, credentials: nil) name ||= deploy_id+"-secret" begin - MU.log "Writing #{name} to S3 bucket #{MU.adminBucketName}" - MU::Cloud::AWS.s3(myRegion).put_object( + MU.log "Writing #{name} to S3 bucket #{adminBucketName(credentials)}" + MU::Cloud::AWS.s3(region: myRegion, credentials: credentials).put_object( acl: "private", - bucket: MU.adminBucketName, + bucket: adminBucketName(credentials), key: name, body: value ) rescue Aws::S3::Errors => e - raise MU::MommaCat::DeployInitializeError, "Got #{e.inspect} trying to write #{name} to #{MU.adminBucketName}" + raise MU::MommaCat::DeployInitializeError, "Got #{e.inspect} trying to write #{name} to #{adminBucketName(credentials)}" end end + # Log bucket policy for enabling CloudTrail logging to our log bucket in S3. + def self.cloudtrailBucketPolicy(credentials = nil) + cfg = credConfig(credentials) + policy_json = '{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AWSCloudTrailAclCheck20131101", + "Effect": "Allow", + "Principal": { + "AWS": "arn:'+(MU::Cloud::AWS.isGovCloud?(cfg['region']) ? "aws-us-gov" : "aws")+':iam::<%= MU.account_number %>:root", + "Service": "cloudtrail.amazonaws.com" + }, + "Action": "s3:GetBucketAcl", + "Resource": "arn:'+(MU::Cloud::AWS.isGovCloud?(cfg['region']) ? "aws-us-gov" : "aws")+':s3:::'+MU::Cloud::AWS.adminBucketName(credentials)+'" + }, + { + "Sid": "AWSCloudTrailWrite20131101", + "Effect": "Allow", + "Principal": { + "AWS": "arn:'+(MU::Cloud::AWS.isGovCloud?(cfg['region']) ? "aws-us-gov" : "aws")+':iam::'+credToAcct(credentials)+':root", + "Service": "cloudtrail.amazonaws.com" + }, + "Action": "s3:PutObject", + "Resource": "arn:'+(MU::Cloud::AWS.isGovCloud?(cfg['region']) ? "aws-us-gov" : "aws")+':s3:::'+MU::Cloud::AWS.adminBucketName(credentials)+'/AWSLogs/'+credToAcct(credentials)+'/*", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control" + } + } + } + ] + }' + ERB.new(policy_json).result + end + @@is_in_aws = nil # Alias for #{MU::Cloud::AWS.hosted?} def self.hosted MU::Cloud::AWS.hosted? @@ -291,55 +362,162 @@ sample["credentials_file"] = "#{Etc.getpwuid(Process.uid).dir}/.aws/credentials" sample["log_bucket_name"] = "my-mu-s3-bucket" sample end - @my_acct_num = nil + @@my_acct_num = nil + @@my_hosted_cfg = nil + @@acct_to_profile_map = {} - # Fetch the AWS account number where this Mu master resides. If it's not in - # AWS at all, or otherwise cannot be determined, return nil. - # here. + # Map the name of a credential set back to an AWS account number + # @param name [String] + def self.credToAcct(name = nil) + creds = credConfig(name) + + return creds['account_number'] if creds['account_number'] + + user_list = MU::Cloud::AWS.iam(credentials: name).list_users.users + acct_num = MU::Cloud::AWS.iam(credentials: name).list_users.users.first.arn.split(/:/)[4] + acct_num.to_s + end + + # Return the name strings of all known sets of credentials for this cloud + # @return [Array<String>] + def self.listCredentials + if !$MU_CFG['aws'] + return hosted? ? ["#default"] : nil + end + + $MU_CFG['aws'].keys + end + + def self.adminBucketName(credentials = nil) + #XXX find a default if this particular account doesn't have a log_bucket_name configured + cfg = credConfig(credentials) + cfg['log_bucket_name'] + end + + def self.adminBucketUrl(credentials = nil) + "s3://"+adminBucketName+"/" + 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 + # an account name is specified which does not exist. + # @param name [String]: The name of the key under 'aws' in mu.yaml to return + # @return [Hash,nil] + 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 hosted? + begin + iam_data = JSON.parse(getAWSMetaData("iam/info")) + if iam_data["InstanceProfileArn"] and !iam_data["InstanceProfileArn"].empty? + @@my_hosted_cfg = hosted_config + return name_only ? "#default" : @@my_hosted_cfg + end + rescue JSON::ParserError => e + end + end + + return nil + end + + if name.nil? + $MU_CFG['aws'].each_pair { |name, cfg| + if cfg['default'] + return name_only ? name : cfg + end + } + else + if $MU_CFG['aws'][name] + return name_only ? name : $MU_CFG['aws'][name] + elsif @@acct_to_profile_map[name.to_s] + return name_only ? name : @@acct_to_profile_map[name.to_s] + elsif name.is_a?(Integer) or name.match(/^\d+$/) + # Try to map backwards from an account id, if that's what we go + $MU_CFG['aws'].each_pair { |acctname, cfg| + if cfg['account_number'] and name.to_s == cfg['account_number'].to_s + return name_only ? acctname : $MU_CFG['aws'][acctname] + end + } + + # Check each credential sets' resident account, then + $MU_CFG['aws'].each_pair { |acctname, cfg| + begin + user_list = MU::Cloud::AWS.iam(credentials: acctname).list_users.users +# rescue ::Aws::IAM::Errors => e # XXX why does this NameError here? + rescue Exception => e + MU.log e.inspect, MU::WARN, details: cfg + next + end + 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" + end + end + + # Fetch the AWS account number where this Mu master resides. If it's not + # in AWS at all, or otherwise cannot be determined, return nil. here. # XXX account for Google and non-cloud situations - # XXX but what about multi-account uuuugh + # XXX this needs to be "myAccountNumber" or somesuch + # XXX and maybe do the IAM thing for arbitrary, non-resident accounts def self.account_number - return nil if !$MU_CFG['aws'] - return @my_acct_num if @my_acct_num + return nil if credConfig.nil? + return @@my_acct_num if @@my_acct_num + loadCredentials +# XXX take optional credential set argument - begin - user_list = MU::Cloud::AWS.iam($MU_CFG['aws']['region']).list_users.users -# rescue ::Aws::IAM::Errors => e # XXX why does this NameError here? - rescue Exception => e - MU.log "Got #{e.inspect} while trying to figure out our account number", MU::WARN, details: caller - end - if user_list.nil? or user_list.size == 0 +# begin +# user_list = MU::Cloud::AWS.iam(region: credConfig['region']).list_users.users +## rescue ::Aws::IAM::Errors => e # XXX why does this NameError here? +# rescue Exception => e +# MU.log "Got #{e.inspect} while trying to figure out our account number", MU::WARN, details: caller +# end +# if user_list.nil? or user_list.size == 0 mac = MU::Cloud::AWS.getAWSMetaData("network/interfaces/macs/").split(/\n/)[0] acct_num = MU::Cloud::AWS.getAWSMetaData("network/interfaces/macs/#{mac}owner-id") acct_num.chomp! - else - acct_num = MU::Cloud::AWS.iam($MU_CFG['aws']['region']).list_users.users.first.arn.split(/:/)[4] - end +# else +# acct_num = MU::Cloud::AWS.iam(region: credConfig['region']).list_users.users.first.arn.split(/:/)[4] +# end MU.setVar("acct_num", acct_num) - @my_acct_num ||= acct_num + @@my_acct_num ||= acct_num acct_num end @@regions = {} # List the Amazon Web Services region names available to this account. The # region that is local to this Mu server will be listed first. # @param us_only [Boolean]: Restrict results to United States only # @return [Array<String>] - def self.listRegions(us_only = false) - if $MU_CFG and (!$MU_CFG['aws'] or !account_number) - return [] - end + def self.listRegions(us_only = false, credentials: nil) + if @@regions.size == 0 - result = MU::Cloud::AWS.ec2(myRegion).describe_regions.regions + return [] if credConfig.nil? + result = MU::Cloud::AWS.ec2(region: myRegion, credentials: credentials).describe_regions.regions regions = [] result.each { |r| - @@regions[r.region_name] = Proc.new { listAZs(r.region_name) } + @@regions[r.region_name] = Proc.new { + listAZs(region: r.region_name, credentials: credentials) + } } end + regions = if us_only @@regions.keys.delete_if { |r| !r.match(/^us\-/) }.uniq else @@regions.keys.uniq end @@ -366,18 +544,18 @@ # Generate an EC2 keypair unique to this deployment, given a regular # OpenSSH-style public key and a name. # @param keyname [String]: The name of the key to create. # @param public_key [String]: The public key # @return [Array<String>]: keypairname, ssh_private_key, ssh_public_key - def self.createEc2SSHKey(keyname, public_key) + def self.createEc2SSHKey(keyname, public_key, credentials: nil) # We replicate this key in all regions if !MU::Cloud::CloudFormation.emitCloudFormation MU::Cloud::AWS.listRegions.each { |region| MU.log "Replicating #{keyname} to EC2 in #{region}", MU::DEBUG, details: @ssh_public_key - MU::Cloud::AWS.ec2(region).import_key_pair( - key_name: keyname, - public_key_material: public_key + MU::Cloud::AWS.ec2(region: region, credentials: credentials).import_key_pair( + key_name: keyname, + public_key_material: public_key ) } end end @@ -387,24 +565,22 @@ # "translate" machine types across cloud providers. # @param region [String]: Supported machine types can vary from region to region, so we look for the set we're interested in specifically # @return [Hash] def self.listInstanceTypes(region = myRegion) return @@instance_types if @@instance_types and @@instance_types[region] - if $MU_CFG and (!$MU_CFG['aws'] or !account_number) - return {} - end + return {} if credConfig.nil? human_region = @@regionLookup[region] @@instance_types ||= {} @@instance_types[region] ||= {} next_token = nil begin # Pricing API isn't widely available, so ask a region we know supports # it - resp = MU::Cloud::AWS.pricing("us-east-1").get_products( + resp = MU::Cloud::AWS.pricing(region: "us-east-1").get_products( service_code: "AmazonEC2", filters: [ { field: "productFamily", value: "Compute Instance", @@ -454,11 +630,11 @@ raise MuError, "Can't call findSSLCertificate without specifying either a name or an id" end if !name.nil? and !name.empty? matches = [] - acmcerts = MU::Cloud::AWS.acm(region).list_certificates( + acmcerts = MU::Cloud::AWS.acm(region: region).list_certificates( certificate_statuses: ["ISSUED"] ) acmcerts.certificate_summary_list.each { |cert| matches << cert.certificate_arn if cert.domain_name == name } @@ -473,18 +649,18 @@ matches << iamcert.server_certificate.server_certificate_metadata.arn end if matches.size == 1 return matches.first elsif matches.size == 0 - raise MuError, "No IAM or ACM certificate named #{name} was found" + raise MuError, "No IAM or ACM certificate named #{name} was found in #{region}" elsif matches.size > 1 - raise MuError, "Multiple certificates named #{name} were found. Remove extras or use ssl_certificate_id to supply the exact ARN of the one you want to use." + raise MuError, "Multiple certificates named #{name} were found in #{region}. Remove extras or use ssl_certificate_id to supply the exact ARN of the one you want to use." end end if id.match(/^arn:aws(?:-us-gov)?:acm/) - resp = MU::Cloud::AWS.acm(region).get_certificate( + resp = MU::Cloud::AWS.acm(region: region).get_certificate( certificate_arn: id ) if resp.nil? raise MuError, "No such ACM certificate '#{id}'" end @@ -508,241 +684,275 @@ id end # Amazon Certificate Manager API - def self.acm(region = MU.curRegion) + def self.acm(region: MU.curRegion, credentials: nil) region ||= myRegion - @@acm_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "ACM", region: region) - @@acm_api[region] + @@acm_api[credentials] ||= {} + @@acm_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "ACM", region: region, credentials: credentials) + @@acm_api[credentials][region] end # Amazon's IAM API - def self.iam(region = MU.curRegion) - region ||= myRegion - @@iam_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "IAM", region: region) - @@iam_api[region] + def self.iam(credentials: nil) + @@iam_api[credentials] ||= MU::Cloud::AWS::Endpoint.new(api: "IAM", credentials: credentials) + @@iam_api[credentials] end # Amazon's EC2 API - def self.ec2(region = MU.curRegion) + def self.ec2(region: MU.curRegion, credentials: nil) region ||= myRegion - @@ec2_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "EC2", region: region) - @@ec2_api[region] + @@ec2_api[credentials] ||= {} + @@ec2_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "EC2", region: region, credentials: credentials) + @@ec2_api[credentials][region] end # Amazon's Autoscaling API - def self.autoscale(region = MU.curRegion) + def self.autoscale(region: MU.curRegion, credentials: nil) region ||= myRegion - @@autoscale_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "AutoScaling", region: region) - @@autoscale_api[region] + @@autoscale_api[credentials] ||= {} + @@autoscale_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "AutoScaling", region: region, credentials: credentials) + @@autoscale_api[credentials][region] end # Amazon's ElasticLoadBalancing API - def self.elb(region = MU.curRegion) + def self.elb(region: MU.curRegion, credentials: nil) region ||= myRegion - @@elb_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "ElasticLoadBalancing", region: region) - @@elb_api[region] + @@elb_api[credentials] ||= {} + @@elb_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "ElasticLoadBalancing", region: region, credentials: credentials) + @@elb_api[credentials][region] end # Amazon's ElasticLoadBalancingV2 (ALB) API - def self.elb2(region = MU.curRegion) + def self.elb2(region: MU.curRegion, credentials: nil) region ||= myRegion - @@elb2_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "ElasticLoadBalancingV2", region: region) - @@elb2_api[region] + @@elb2_api[credentials] ||= {} + @@elb2_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "ElasticLoadBalancingV2", region: region, credentials: credentials) + @@elb2_api[credentials][region] end # Amazon's Route53 API - def self.route53(region = MU.curRegion) - region ||= myRegion - @@route53_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "Route53", region: region) - @@route53_api[region] + def self.route53(credentials: nil) + @@route53_api[credentials] ||= MU::Cloud::AWS::Endpoint.new(api: "Route53", credentials: credentials) + @@route53_api[credentials] end # Amazon's RDS API - def self.rds(region = MU.curRegion) + def self.rds(region: MU.curRegion, credentials: nil) region ||= myRegion - @@rds_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "RDS", region: region) - @@rds_api[region] + @@rds_api[credentials] ||= {} + @@rds_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "RDS", region: region, credentials: credentials) + @@rds_api[credentials][region] end # Amazon's CloudFormation API - def self.cloudformation(region = MU.curRegion) + def self.cloudformation(region: MU.curRegion, credentials: nil) region ||= myRegion - @@cloudformation_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudFormation", region: region) - @@cloudformation_api[region] + @@cloudformation_api[credentials] ||= {} + @@cloudformation_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudFormation", region: region, credentials: credentials) + @@cloudformation_api[credentials][region] end # Amazon's S3 API - def self.s3(region = MU.curRegion) + def self.s3(region: MU.curRegion, credentials: nil) region ||= myRegion - @@s3_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "S3", region: region) - @@s3_api[region] + @@s3_api[credentials] ||= {} + @@s3_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "S3", region: region, credentials: credentials) + @@s3_api[credentials][region] end # Amazon's CloudTrail API - def self.cloudtrail(region = MU.curRegion) + def self.cloudtrail(region: MU.curRegion, credentials: nil) region ||= myRegion - @@cloudtrail_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudTrail", region: region) - @@cloudtrail_api[region] + @@cloudtrail_api[credentials] ||= {} + @@cloudtrail_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudTrail", region: region, credentials: credentials) + @@cloudtrail_api[credentials][region] end # Amazon's CloudWatch API - def self.cloudwatch(region = MU.curRegion) + def self.cloudwatch(region: MU.curRegion, credentials: nil) region ||= myRegion - @@cloudwatch_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudWatch", region: region) - @@cloudwatch_api[region] + @@cloudwatch_api[credentials] ||= {} + @@cloudwatch_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudWatch", region: region, credentials: credentials) + @@cloudwatch_api[credentials][region] end # Amazon's Web Application Firewall API (Global, for CloudFront et al) - def self.wafglobal(region = MU.curRegion) + def self.wafglobal(region: MU.curRegion, credentials: nil) region ||= myRegion - @@wafglobal[region] ||= MU::Cloud::AWS::Endpoint.new(api: "WAF", region: region) - @@wafglobal[region] + @@wafglobal_api[credentials] ||= {} + @@wafglobal[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "WAF", region: region, credentials: credentials) + @@wafglobal[credentials][region] end # Amazon's Web Application Firewall API (Regional, for ALBs et al) - def self.waf(region = MU.curRegion) + def self.waf(region: MU.curRegion, credentials: nil) region ||= myRegion - @@waf[region] ||= MU::Cloud::AWS::Endpoint.new(api: "WAFRegional", region: region) - @@waf[region] + @@waf[credentials] ||= {} + @@waf[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "WAFRegional", region: region, credentials: credentials) + @@waf[credentials][region] end # Amazon's CloudWatchLogs API - def self.cloudwatchlogs(region = MU.curRegion) + def self.cloudwatchlogs(region: MU.curRegion, credentials: nil) region ||= myRegion - @@cloudwatchlogs_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudWatchLogs", region: region) - @@cloudwatchlogs_api[region] + @@cloudwatchlogs_api[credentials] ||= {} + @@cloudwatchlogs_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudWatchLogs", region: region, credentials: credentials) + @@cloudwatchlogs_api[credentials][region] end # Amazon's CloudFront API - def self.cloudfront(region = MU.curRegion) + def self.cloudfront(region: MU.curRegion, credentials: nil) region ||= myRegion - @@cloudfront_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudFront", region: region) - @@cloudfront_api[region] + @@cloudfront_api[credentials] ||= {} + @@cloudfront_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudFront", region: region, credentials: credentials) + @@cloudfront_api[credentials][region] end # Amazon's ElastiCache API - def self.elasticache(region = MU.curRegion) + def self.elasticache(region: MU.curRegion, credentials: nil) region ||= myRegion - @@elasticache_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "ElastiCache", region: region) - @@elasticache_api[region] + @@elasticache_api[credentials] ||= {} + @@elasticache_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "ElastiCache", region: region, credentials: credentials) + @@elasticache_api[credentials][region] end # Amazon's SNS API - def self.sns(region = MU.curRegion) + def self.sns(region: MU.curRegion, credentials: nil) region ||= myRegion - @@sns_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "SNS", region: region) - @@sns_api[region] + @@sns_api[credentials] ||= {} + @@sns_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "SNS", region: region, credentials: credentials) + @@sns_api[credentials][region] end # Amazon's SQS API - def self.sqs(region = MU.curRegion) + def self.sqs(region: MU.curRegion, credentials: nil) region ||= myRegion - @@sqs_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "SQS", region: region) - @@sqs_api[region] + @@sqs_api[credentials] ||= {} + @@sqs_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "SQS", region: region, credentials: credentials) + @@sqs_api[credentials][region] end # Amazon's EFS API - def self.efs(region = MU.curRegion) + def self.efs(region: MU.curRegion, credentials: nil) region ||= myRegion - @@efs_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "EFS", region: region) - @@efs_api[region] + @@efs_api[credentials] ||= {} + @@efs_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "EFS", region: region, credentials: credentials) + @@efs_api[credentials][region] end # Amazon's Lambda API - def self.lambda(region = MU.curRegion) + def self.lambda(region: MU.curRegion, credentials: nil) region ||= myRegion - @@lambda_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "Lambda", region: region) - @@lambda_api[region] + @@lambda_api[credentials] ||= {} + @@lambda_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "Lambda", region: region, credentials: credentials) + @@lambda_api[credentials][region] end # Amazon's API Gateway API - def self.apig(region = MU.curRegion) + def self.apig(region: MU.curRegion, credentials: nil) region ||= myRegion - @@apig_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "APIGateway", region: region) - @@apig_api[region] + @@apig_api[credentials] ||= {} + @@apig_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "APIGateway", region: region, credentials: credentials) + @@apig_api[credentials][region] end # Amazon's Cloudwatch Events API def self.cloudwatch_events(region = MU.cureRegion) region ||= myRegion - @@cloudwatch_events_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudWatchEvents", region: region) + @@cloudwatch_events_api[credentials] ||= {} + @@cloudwatch_events_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "CloudWatchEvents", region: region, credentials: credentials) @@cloudwatch_events_api end # Amazon's ECS API - def self.ecs(region = MU.curRegion) + def self.ecs(region: MU.curRegion, credentials: nil) region ||= myRegion - @@ecs_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "ECS", region: region) - @@ecs_api[region] + @@ecs_api[credentials] ||= {} + @@ecs_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "ECS", region: region, credentials: credentials) + @@ecs_api[credentials][region] end # Amazon's EKS API - def self.eks(region = MU.curRegion) + def self.eks(region: MU.curRegion, credentials: nil) region ||= myRegion - @@eks_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "EKS", region: region) - @@eks_api[region] + @@eks_api[credentials] ||= {} + @@eks_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "EKS", region: region, credentials: credentials) + @@eks_api[credentials][region] end # Amazon's Pricing API - def self.pricing(region = MU.curRegion) + def self.pricing(region: MU.curRegion, credentials: nil) region ||= myRegion - @@pricing_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "Pricing", region: region) - @@pricing_api[region] + @@pricing_api[credentials] ||= {} + @@pricing_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "Pricing", region: region, credentials: credentials) + @@pricing_api[credentials][region] end # Amazon's Simple Systems Manager API - def self.ssm(region = MU.curRegion) + def self.ssm(region: MU.curRegion, credentials: nil) region ||= myRegion - @@ssm_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "SSM", region: region) - @@ssm_api[region] + @@ssm_api[credentials] ||= {} + @@ssm_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "SSM", region: region, credentials: credentials) + @@ssm_api[credentials][region] end # Amazon's Elasticsearch API - def self.elasticsearch(region = MU.curRegion) + def self.elasticsearch(region: MU.curRegion, credentials: nil) region ||= myRegion - @@elasticsearch_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "ElasticsearchService", region: region) - @@elasticsearch_api[region] + @@elasticsearch_api[credentials] ||= {} + @@elasticsearch_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "ElasticsearchService", region: region, credentials: credentials) + @@elasticsearch_api[credentials][region] end # Amazon's Cognito Identity API - def self.cognito_ident(region = MU.curRegion) + def self.cognito_ident(region: MU.curRegion, credentials: nil) region ||= myRegion - @@cognito_ident_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "CognitoIdentity", region: region) - @@cognito_ident_api[region] + @@cognito_ident_api[credentials] ||= {} + @@cognito_ident_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "CognitoIdentity", region: region, credentials: credentials) + @@cognito_ident_api[credentials][region] end # Amazon's Cognito Identity Provider API - def self.cognito_user(region = MU.curRegion) + def self.cognito_user(region: MU.curRegion, credentials: nil) region ||= myRegion - @@cognito_user_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "CognitoIdentityProvider", region: region) - @@cognito_user_api[region] + @@cognito_user_api[credentials] ||= {} + @@cognito_user_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "CognitoIdentityProvider", region: region, credentials: credentials) + @@cognito_user_api[credentials][region] end # Amazon's KMS API - def self.kms(region = MU.curRegion) + def self.kms(region: MU.curRegion, credentials: nil) region ||= myRegion - @@kms_api[region] ||= MU::Cloud::AWS::Endpoint.new(api: "KMS", region: region) - @@kms_api[region] + @@kms_api[credentials] ||= {} + @@kms_api[credentials][region] ||= MU::Cloud::AWS::Endpoint.new(api: "KMS", region: region, credentials: credentials) + @@kms_api[credentials][region] end + # Amazon's Organizations API + def self.orgs(credentials: nil) + @@organizations_api ||= {} + @@organizations_api[credentials] ||= MU::Cloud::AWS::Endpoint.new(api: "Organizations", credentials: credentials) + @@organizations_api[credentials] + end + # Fetch an Amazon instance metadata parameter (example: public-ipv4). # @param param [String]: The parameter name to fetch # @return [String, nil] def self.getAWSMetaData(param) base_url = "http://169.254.169.254/latest/meta-data/" begin response = nil - Timeout.timeout(2) do + Timeout.timeout(1) do response = open("#{base_url}/#{param}").read end response rescue OpenURI::HTTPError, Timeout::Error, SocketError, Errno::ENETUNREACH, Net::HTTPServerException, Errno::EHOSTUNREACH => e - # This is fairly normal, just handle it gracefully + # This is normal on machines checking to see if they're AWS-hosted logger = MU::Logger.new logger.log "Failed metadata request #{base_url}/#{param}: #{e.inspect}", MU::DEBUG return nil end end @@ -842,11 +1052,11 @@ group.ip_permissions.each { |rule| if rule.ip_protocol == "tcp" and rule.from_port == port and rule.to_port == port MU.log "Revoking old rules for port #{port.to_s} from #{sg_id}", MU::NOTICE begin - MU::Cloud::AWS.ec2(myRegion).revoke_security_group_ingress( + MU::Cloud::AWS.ec2(region: myRegion).revoke_security_group_ingress( group_id: sg_id, ip_permissions: [ { ip_protocol: "tcp", from_port: port, @@ -869,11 +1079,11 @@ allow_ips.each { |cidr| allow_ips_cidr << {"cidr_ip" => cidr} } begin - MU::Cloud::AWS.ec2(myRegion).authorize_security_group_ingress( + MU::Cloud::AWS.ec2(region: myRegion).authorize_security_group_ingress( group_id: sg_id, ip_permissions: [ { ip_protocol: "tcp", from_port: 10514, @@ -917,28 +1127,45 @@ # Wrapper class for the EC2 API, so that we can catch some common transient # endpoint errors without having to spray rescues all over the codebase. class Endpoint @api = nil @region = nil + @cred_obj = nil + attr_reader :credentials + attr_reader :account # Create an AWS API client # @param region [String]: Amazon region so we know what endpoint to use # @param api [String]: Which API are we wrapping? - def initialize(region: MU.curRegion, api: "EC2") - @region = region + def initialize(region: MU.curRegion, api: "EC2", credentials: nil) + @cred_obj = MU::Cloud::AWS.loadCredentials(credentials) + @credentials = MU::Cloud::AWS.credConfig(credentials, name_only: true) + + if !@cred_obj + raise MuError, "Unable to locate valid AWS credentials for #{api} API. #{credentials ? "Credentials requested were '#{credentials}'": ""}" + end + + params = {} + if region - @api = Object.const_get("Aws::#{api}::Client").new(region: region) - else - @api = Object.const_get("Aws::#{api}::Client").new + @region = region + params[:region] = @region end + + 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. def method_missing(method_sym, *arguments) - MU::Cloud::AWS.loadCredentials + retries = 0 begin MU.log "Calling #{method_sym} in #{@region}", MU::DEBUG, details: arguments retval = nil if !arguments.nil? and arguments.size == 1 @@ -947,11 +1174,11 @@ 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 => 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 => 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 @@ -965,13 +1192,16 @@ debuglevel = MU::WARN interval = 40 + Random.rand(15) - 5 # elsif retries > 100 # raise MuError, "Exhausted retries after #{retries} attempts while calling EC2's #{method_sym} in #{@region}. Args were: #{arguments}" end - MU.log "Got #{e.inspect} calling EC2's #{method_sym} in #{@region}, waiting #{interval.to_s}s and retrying. Args were: #{arguments}", debuglevel, details: caller + MU.log "Got #{e.inspect} calling EC2's #{method_sym} in #{@region} with credentials #{@credentials}, waiting #{interval.to_s}s and retrying. Args were: #{arguments}", debuglevel, details: caller sleep interval retry + rescue Exception => e + MU.log "Got #{e.inspect} calling EC2's #{method_sym} in #{@region} with credentials #{@credentials}", MU::DEBUG, details: arguments + raise e end end end @@iam_api = {} @@acm_api = {} @@ -1002,8 +1232,9 @@ @@ssm_api ={} @@elasticsearch_api ={} @@cognito_ident_api ={} @@cognito_user_api ={} @@kms_api ={} + @@organizataion_api ={} end end end