class MU::Cloud::AWS::Server

A server as configured in {MU::Config::BasketofKittens::servers}

Public Class Methods

associateElasticIp(instance_id, classic: false, ip: nil, region: MU.curRegion, credentials: nil) click to toggle source

Associate an Amazon Elastic IP with an instance. @param instance_id [String]: The cloud provider identifier of the instance. @param classic [Boolean]: Whether to assume we're using an IP in EC2 Classic instead of VPC. @param ip [String]: Request a specific IP address. @param region [String]: The cloud provider region @return [void]

# File modules/mu/providers/aws/server.rb, line 1394
def self.associateElasticIp(instance_id, classic: false, ip: nil, region: MU.curRegion, credentials: nil)
  MU.log "associateElasticIp called: #{instance_id}, classic: #{classic}, ip: #{ip}, region: #{region}", MU::DEBUG
  elastic_ip = nil
  @eip_semaphore.synchronize {
    if !ip.nil?
      filters = [{name: "public-ip", values: [ip]}]
      resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_addresses(filters: filters)
      if @eips_used.include?(ip)
        is_free = false
        resp.addresses.each { |address|
          if address.public_ip == ip and (address.instance_id.nil? and address.network_interface_id.nil?) or address.instance_id == instance_id
            @eips_used.delete(ip)
            is_free = true
          end
        }

        raise MuError, "Requested EIP #{ip}, but we've already assigned this IP to someone else" if !is_free
      else
        resp.addresses.each { |address|
          if address.public_ip == ip and address.instance_id == instance_id
            return ip
          end
        }
      end
    end
    elastic_ip = findFreeElasticIp(classic: classic, ip: ip, credentials: credentials)
    if !ip.nil? and (elastic_ip.nil? or ip != elastic_ip.public_ip)
      raise MuError, "Requested EIP #{ip}, but this IP does not exist or is not available"
    end
    if elastic_ip.nil?
      raise MuError, "Couldn't find an Elastic IP to associate with #{instance_id}"
    end
    @eips_used << elastic_ip.public_ip
    MU.log "Associating Elastic IP #{elastic_ip.public_ip} with #{instance_id}", details: elastic_ip
  }

  on_retry = Proc.new { |e|
    if e.class == Aws::EC2::Errors::ResourceAlreadyAssociated
      # A previous association attempt may have succeeded, albeit slowly.
      resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_addresses(
        allocation_ids: [elastic_ip.allocation_id]
      )
      first_addr = resp.addresses.first
      if first_addr and first_addr.instance_id != instance_id
        raise MuError, "Tried to associate #{elastic_ip.public_ip} with #{instance_id}, but it's already associated with #{first_addr.instance_id}!"
      end
    end
  }

  MU.retrier([Aws::EC2::Errors::IncorrectInstanceState, Aws::EC2::Errors::ResourceAlreadyAssociated], wait: 5, max: 6, on_retry: on_retry) {
    if classic
      MU::Cloud::AWS.ec2(region: region, credentials: credentials).associate_address(
        instance_id: instance_id,
        public_ip: elastic_ip.public_ip
      )
    else
      MU::Cloud::AWS.ec2(region: region, credentials: credentials).associate_address(
        instance_id: instance_id,
        allocation_id: elastic_ip.allocation_id,
        allow_reassociation: false
      )
    end
  }

  loop_if = Proc.new {
    instance = find(cloud_id: instance_id, region: region, credentials: credentials).values.first
    instance.public_ip_address != elastic_ip.public_ip
  }
  MU.retrier(loop_if: loop_if, wait: 10, max: 3) {
    MU.log "Waiting for Elastic IP association of #{elastic_ip.public_ip} to #{instance_id} to take effect", MU::NOTICE
  }

  MU.log "Elastic IP #{elastic_ip.public_ip} now associated with #{instance_id}"

  return elastic_ip.public_ip
end
cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {}) click to toggle source

Remove all instances associated with the currently loaded deployment. Also cleans up associated volumes, droppings in the MU master's /etc/hosts and ~/.ssh, and in whatever Groomer was used. @param noop [Boolean]: If true, will only print what would be done @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server @param region [String]: The cloud provider region @return [void]

# File modules/mu/providers/aws/server.rb, line 1489
def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
  onlycloud = flags["onlycloud"]
  skipsnapshots = flags["skipsnapshots"]
  tagfilters = [
    {name: "tag:MU-ID", values: [deploy_id]}
  ]
  if !ignoremaster
    tagfilters << {name: "tag:MU-MASTER-IP", values: [MU.mu_public_ip]}
  end
  unterminated = Array.new
  name_tags = Array.new

  # Build a list of instances we need to clean up. We guard against
  # accidental deletion here by requiring someone to have hand-terminated
  # these, by default.
  resp = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_instances(
      filters: tagfilters
  )

  return if resp.data.reservations.nil?
  resp.data.reservations.each { |reservation|
    reservation.instances.each { |instance|
      if instance.state.name != "terminated"
        unterminated << instance
        instance.tags.each { |tag|
          name_tags << tag.value if tag.key == "Name"
        }
      end
    }
  }

  parent_thread_id = Thread.current.object_id

  threads = []
  unterminated.each { |instance|
    threads << Thread.new(instance) { |myinstance|
      MU.dupGlobals(parent_thread_id)
      Thread.abort_on_exception = true
      MU::Cloud::AWS::Server.terminateInstance(id: myinstance.instance_id, noop: noop, onlycloud: onlycloud, region: region, deploy_id: deploy_id, credentials: credentials)
    }
  }

  resp = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_volumes(
      filters: tagfilters
  )
  resp.data.volumes.each { |volume|
    threads << Thread.new(volume) { |myvolume|
      MU.dupGlobals(parent_thread_id)
      Thread.abort_on_exception = true
      delete_volume(myvolume, noop, skipsnapshots, credentials: credentials, deploy_id: deploy_id)
    }
  }

  # Wait for all of the instances to finish cleanup before proceeding
  threads.each { |t|
    t.join
  }
end
configureBlockDevices(image_id: nil, storage: nil, add_ephemeral: true, region: MU.myRegion, credentials: nil) click to toggle source

Given some combination of a base image, BoK-configured storage, and ephemeral devices, return the structure passed to EC2 to declare block devicde mappings. @param image_id [String] @param storage [Array] @param add_ephemeral [Boolean] @param region [String] @param credentials [String]

# File modules/mu/providers/aws/server.rb, line 1980
def self.configureBlockDevices(image_id: nil, storage: nil, add_ephemeral: true, region: MU.myRegion, credentials: nil)
  ext_disks = {}
  
  # Figure out which devices are embedded in the AMI already.
  if image_id
    image = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_images(image_ids: [image_id]).images.first
    if !image.block_device_mappings.nil?
      image.block_device_mappings.each { |disk|
        if !disk.device_name.nil? and !disk.device_name.empty? and !disk.ebs.nil? and !disk.ebs.empty?
          ext_disks[disk.device_name] = MU.structToHash(disk.ebs)
        end
      }
    end
  end
  
  configured_storage = []
  if storage
    storage.each { |vol|
      # Drop the "encrypted" flag if a snapshot for this device exists
      # in the AMI, even if they both agree about the value of said
      # flag. Apparently that's a thing now.
      if ext_disks.has_key?(vol["device"])
        if ext_disks[vol["device"]].has_key?(:snapshot_id)
          vol.delete("encrypted")
        end
      end
      mapping, _cfm_mapping = MU::Cloud::AWS::Server.convertBlockDeviceMapping(vol)
      configured_storage << mapping
    }
  end
  
  configured_storage.concat(@ephemeral_mappings) if add_ephemeral
  
  configured_storage
end
convertBlockDeviceMapping(storage) click to toggle source

Maps our configuration language's 'storage' primitive to an Amazon-style block_device_mapping. @param storage [Hash]: The {MU::Config}-style storage description. @return [Hash]: The Amazon-style storage description.

# File modules/mu/providers/aws/server.rb, line 1128
def self.convertBlockDeviceMapping(storage)
  vol_struct = {}
  cfm_mapping = {}
  if storage["no_device"]
    vol_struct[:no_device] = storage["no_device"]
    cfm_mapping["NoDevice"] = storage["no_device"]
  end

  if storage["device"]
    vol_struct[:device_name] = storage["device"]
    cfm_mapping["DeviceName"] = storage["device"]
  elsif storage["no_device"].nil?
    vol_struct[:device_name] = @disk_devices.shift
    cfm_mapping["DeviceName"] = @disk_devices.shift
  end

  vol_struct[:virtual_name] = storage["virtual_name"] if storage["virtual_name"]

  storage["volume_size"] = storage["size"]
  if storage["snapshot_id"] or storage["size"]
    vol_struct[:ebs] = {}
    cfm_mapping["Ebs"] = {}
    [:delete_on_termination, :snapshot_id, :volume_size, :volume_type, :encrypted].each { |arg|
      if storage.has_key?(arg.to_s) and !storage[arg.to_s].nil?
        vol_struct[:ebs][arg] = storage[arg.to_s]
        key = ""
        arg.to_s.split(/_/).each { |chunk| key = key + chunk.capitalize }
        cfm_mapping["Ebs"][key] = storage[arg.to_s]
      end
    }
    cfm_mapping["Ebs"].delete("Encrypted") if !cfm_mapping["Ebs"]["Encrypted"]

    if storage["iops"] and storage["volume_type"] == "io1"
      vol_struct[:ebs][:iops] = storage["iops"] 
      cfm_mapping["Ebs"]["Iops"] = storage["iops"]
    end
  end

  return [vol_struct, cfm_mapping]
end
createImage(name: nil, instance_id: nil, storage: {}, exclude_storage: false, make_public: false, region: MU.curRegion, copy_to_regions: [], tags: [], credentials: nil) click to toggle source

Create an AMI out of a running server. Requires either the name of a MU resource in the current deployment, or the cloud provider id of a running instance. @param name [String]: The MU resource name of the server to use as the basis for this image. @param instance_id [String]: The cloud provider resource identifier of the server to use as the basis for this image. @param storage [Hash]: The storage devices to include in this image. @param exclude_storage [Boolean]: Do not include the storage device profile of the running instance when creating this image. @param region [String]: The cloud provider region @param copy_to_regions [Array<String>]: Copy the resulting AMI into the listed regions. @param tags [Array<String>]: Extra/override tags to apply to the image. @return [String]: The cloud provider identifier of the new machine image.

# File modules/mu/providers/aws/server.rb, line 992
def self.createImage(name: nil, instance_id: nil, storage: {}, exclude_storage: false, make_public: false, region: MU.curRegion, copy_to_regions: [], tags: [], credentials: nil)
  ami_descriptor = {
    :instance_id => instance_id,
    :name => name,
    :description => "Image automatically generated by Mu from #{name}"
  }
  ami_ids = {}

  storage_list = Array.new
  if exclude_storage
    instance = MU::Cloud::Server.find(cloud_id: instance_id, region: region)
    instance.block_device_mappings.each { |vol|
      if vol.device_name != instance.root_device_name
        
        storage_list << MU::Cloud::AWS::Server.convertBlockDeviceMapping(
            {
                "device" => vol.device_name,
                "no-device" => ""
            }
        )[0]
      end
    }
  elsif !storage.nil?
    storage.each { |vol|
      storage_list << MU::Cloud::AWS::Server.convertBlockDeviceMapping(vol)[0]
    }
  end
  ami_descriptor[:block_device_mappings] = storage_list
  if !exclude_storage
    ami_descriptor[:block_device_mappings].concat(@ephemeral_mappings)
  end
  MU.log "Creating AMI from #{name}", details: ami_descriptor
  resp = nil
  begin
    resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).create_image(ami_descriptor)
  rescue Aws::EC2::Errors::InvalidAMINameDuplicate
    MU.log "AMI #{name} already exists, skipping", MU::WARN
    return nil
  end

  ami = resp.image_id

  ami_ids[region] = ami
  MU::Cloud::AWS.createStandardTags(ami, region: region, credentials: credentials)
  MU::Cloud::AWS.createTag(ami, "Name", name, region: region, credentials: credentials)
  MU.log "AMI of #{name} in region #{region}: #{ami}"
  if make_public
    MU::Cloud::AWS::Server.waitForAMI(ami, region: region, credentials: credentials)
    MU::Cloud::AWS.ec2(region: region, credentials: credentials).modify_image_attribute(
        image_id: ami,
        launch_permission: {add: [{group: "all"}]},
        attribute: "launchPermission"
    )
  end
  copythreads = []
  if !copy_to_regions.nil? and copy_to_regions.size > 0
    parent_thread_id = Thread.current.object_id
    MU::Cloud::AWS::Server.waitForAMI(ami, region: region, credentials: credentials) if !make_public
    copy_to_regions.each { |r|
      next if r == region
      copythreads << Thread.new {
        MU.dupGlobals(parent_thread_id)
        copy = MU::Cloud::AWS.ec2(region: r, credentials: credentials).copy_image(
            source_region: region,
            source_image_id: ami,
            name: name,
            description: "Image automatically generated by Mu from #{name}"
        )
        MU.log "Initiated copy of #{ami} from #{region} to #{r}: #{copy.image_id}"
        ami_ids[r] = copy.image_id

        MU::Cloud::AWS.createStandardTags(copy.image_id, region: r, credentials: credentials)
        MU::Cloud::AWS.createTag(copy.image_id, "Name", name, region: r, credentials: credentials)
        if !tags.nil?
          tags.each { |tag|
            MU::Cloud::AWS.createTag(instance.instance_id, tag['key'], tag['value'], region: r, credentials: credentials)
          }
        end
        MU::Cloud::AWS::Server.waitForAMI(copy.image_id, region: r, credentials: credentials)
        if make_public
          MU::Cloud::AWS.ec2(region: r, credentials: credentials).modify_image_attribute(
              image_id: copy.image_id,
              launch_permission: {add: [{group: "all"}]},
              attribute: "launchPermission"
          )
        end
        MU.log "AMI of #{name} in region #{r}: #{copy.image_id}"
      } # Thread
    }
  end

  copythreads.each { |t|
    t.join
  }

  return ami_ids
end
disk_devices() click to toggle source

List of standard disk device names to present to instances. @return [Array<String>]

# File modules/mu/providers/aws/server.rb, line 47
def self.disk_devices
  @disk_devices
end
ephemeral_mappings() click to toggle source

Ephemeral storage device mappings. Useful for AMIs that don't do this for us. @return [Hash]

# File modules/mu/providers/aws/server.rb, line 74
def self.ephemeral_mappings
  @ephemeral_mappings
end
fetchUserdata(platform: "linux", template_variables: {}, custom_append: nil, scrub_mu_isms: false) click to toggle source

Fetch our baseline userdata argument (read: “script that runs on first boot”) for a given platform. XXX both the eval() and the blind File.read() based on the platform 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]

# File modules/mu/providers/aws/server.rb, line 139
def self.fetchUserdata(platform: "linux", template_variables: {}, custom_append: nil, scrub_mu_isms: false)
  return nil if platform.nil? or platform.empty?
  @@userdata_semaphore.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
      $mu = OpenStruct.new(template_variables)
      userdata_dir = File.expand_path(MU.myRoot+"/modules/mu/providers/aws/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
      erbfile = "#{userdata_dir}/#{platform}.erb"
      if !File.exist?(erbfile)
        MU.log "No such userdata template '#{erbfile}'", MU::WARN, details: caller
        return ""
      end
      userdata = File.read(erbfile)
      begin
        erb = ERB.new(userdata, nil, "<>")
        script = erb.result
      rescue NameError => e
        raise MuError, "Error parsing userdata script #{erbfile} as an ERB template: #{e.inspect}"
      end
      MU.log "Parsed #{erbfile} as ERB", MU::DEBUG, details: script
    end

    if !custom_append.nil?
      if custom_append['path'].nil?
        raise MuError, "Got a custom userdata script argument, but no ['path'] component"
      end
      erbfile = File.read(custom_append['path'])
      MU.log "Loaded userdata script from #{custom_append['path']}"
      if custom_append['use_erb']
        begin
          erb = ERB.new(erbfile, 1, "<>")
          if custom_append['skip_std']
            script = +erb.result
          else
            script = script+"\n"+erb.result
          end
        rescue NameError => e
          raise MuError, "Error parsing userdata script #{erbfile} as an ERB template: #{e.inspect}"
        end
        MU.log "Parsed #{custom_append['path']} as ERB", MU::DEBUG, details: script
      else
        if custom_append['skip_std']
          script = erbfile
        else
          script = script+"\n"+erbfile
        end
        MU.log "Parsed #{custom_append['path']} as flat file", MU::DEBUG, details: script
      end
    end
    return script
  }
end
find(**args) click to toggle source

Locate an existing instance or instances and return an array containing matching AWS resource descriptors for those that match. @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching instances

# File modules/mu/providers/aws/server.rb, line 559
def self.find(**args)
  ip ||= args[:flags]['ip'] if args[:flags] and args[:flags]['ip']

  regions = args[:region].nil? ? MU::Cloud::AWS.listRegions : [args[:region]]

  found = {}
  search_semaphore = Mutex.new
  search_threads = []

  base_filter = { name: "instance-state-name", values: ["running", "pending", "stopped"] }
  searches = []

  if args[:cloud_id]
    searches << {
      :instance_ids => [args[:cloud_id]],
      :filters => [base_filter]
    }
  end

  if ip
    ["ip-address", "private-ip-address"].each { |ip_type|
      searches << {
        filters: [base_filter,  {name: ip_type, values: [ip]} ],
      }
    }
  end

  if args[:tag_value] and args[:tag_key]
    searches << {
      filters: [
        base_filter,
        {name: ip_type, values: [ip]},
        {name: "tag:#{args[:tag_key]}", values: [args[:tag_value]]},
      ]
    }
  end

  if searches.empty?
    searches << { filters: [base_filter] }
  end

  regions.each { |r|
    searches.each { |search|
      search_threads << Thread.new(search) { |params|
        MU.retrier([], wait: 5, max: 5, ignoreme: [Aws::EC2::Errors::InvalidInstanceIDNotFound]) {
          MU::Cloud::AWS.ec2(region: r, credentials: args[:credentials]).describe_instances(params).reservations.each { |resp|
            next if resp.nil? or resp.instances.nil?
            resp.instances.each { |i|
              search_semaphore.synchronize {
                found[i.instance_id] = i
              }
            }
          }
        }
      }
    }
  }
  done_threads = []
  begin
    search_threads.each { |t|
      joined = t.join(2)
      done_threads << joined if !joined.nil?
    }
  end while found.size < 1 and done_threads.size != search_threads.size

  return found
end
findFreeElasticIp(classic: false, ip: nil, region: MU.curRegion, credentials: nil) click to toggle source

Find a free AWS Elastic IP. @param classic [Boolean]: Toggle whether to allocate an IP in EC2 Classic instead of VPC. @param ip [String]: Request a specific IP address. @param region [String]: The cloud provider region

# File modules/mu/providers/aws/server.rb, line 1238
def self.findFreeElasticIp(classic: false, ip: nil, region: MU.curRegion, credentials: nil)
  filters = Array.new
  if !classic
    filters << {name: "domain", values: ["vpc"]}
  else
    filters << {name: "domain", values: ["standard"]}
  end
  filters << {name: "public-ip", values: [ip]} if ip != nil

  if filters.size > 0
    resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_addresses(filters: filters)
  else
    resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_addresses
  end
  resp.addresses.each { |address|
    return address if (address.network_interface_id.nil? or address.network_interface_id.empty?) or !@eips_used.include?(address.public_ip)
  }
  if !ip.nil?
    mode = classic ? "EC2 Classic" : "VPC"
    raise MuError.new "Requested EIP #{ip}, but no such IP exists or is available in #{mode} mode#{credentials ? " with credentials #{credentials}" : ""}", details: { "describe_address filters" => filters, "describe_address response" => resp }
  end
  if !classic
    resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).allocate_address(domain: "vpc")
    new_ip = resp.public_ip
  else
    new_ip = MU::Cloud::AWS.ec2(region: region, credentials: credentials).allocate_address().public_ip
  end
  filters = [{name: "public-ip", values: [new_ip]}]
  if resp.domain
    filters << {name: "domain", values: [resp.domain]}
  end rescue NoMethodError
  if new_ip.nil?
    MU.log "Unable to allocate new Elastic IP. Are we at quota?", MU::ERR
    raise MuError, "Unable to allocate new Elastic IP. Are we at quota?"
  end
  MU.log "Allocated new EIP #{new_ip}, fetching full description"


  begin
    begin
      sleep 5
      resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_addresses(
        filters: filters
      )
      addr = resp.addresses.first
    end while resp.addresses.size < 1 or addr.public_ip.nil?
  rescue NoMethodError
    MU.log "EIP descriptor came back without a public_ip attribute for #{new_ip}, retrying", MU::WARN
    sleep 5
    retry
  end

  return addr
end
generateStandardRole(server, configurator) click to toggle source

Boilerplate generation of an instance role @param server [Hash]: The BoK-style config hash for a Server or ServerPool @param configurator [MU::Config]

# File modules/mu/providers/aws/server.rb, line 1814
        def self.generateStandardRole(server, configurator)
          role = {
            "name" => server["name"],
            "bare_policies" => !server['generate_iam_role'],
            "strip_path" => server["role_strip_path"],
            "can_assume" => [
              {
                "entity_id" => "ec2.amazonaws.com",
                "entity_type" => "service"
              }
            ],
            "policies" => [
              {
                "name" => "MuSecrets",
                "permissions" => ["s3:GetObject"],
                "targets" => [
                  {
                    "identifier" => 'arn:'+(MU::Cloud::AWS.isGovCloud?(server['region']) ? "aws-us-gov" : "aws")+':s3:::'+MU::Cloud::AWS.adminBucketName(server['credentials'])+'/Mu_CA.pem'
                  }
                ]
              }
            ]
          }
          role["credentials"] = server["credentials"] if server["credentials"]
          if server['iam_policies']
            role['iam_policies'] = server['iam_policies'].dup
          end
          if server['canned_iam_policies']
            role['import'] = server['canned_iam_policies'].dup
          end
          if server['iam_role']
# XXX maybe break this down into policies and add those?
          end

          configurator.insertKitten(role, "roles")
          MU::Config.addDependency(server, server["name"], "role")
        end
genericNAT() click to toggle source

Return a BoK-style config hash describing a NAT instance. We use this to approximate NAT gateway functionality with a plain instance. @return [Hash]

# File modules/mu/providers/aws/server.rb, line 1660
def self.genericNAT
  return {
    "cloud" => "AWS",
    "bastion" => true,
    "size" => "t2.small",
    "run_list" => [ "mu-nat" ],
    "groomer" => "Ansible",
    "platform" => "centos7",
    "ssh_user" => "centos",
    "associate_public_ip" => true,
    "static_ip" => { "assign_ip" => true },
  }
end
getAddresses(instance = nil, id: nil, region: MU.curRegion, credentials: nil) click to toggle source

Return an instance's AWS-assigned IP addresses and hostnames. @param instance [OpenStruct] @param id [String] @param region [String] @param credentials [@String] @return [Array<Array>]

# File modules/mu/providers/aws/server.rb, line 1554
def self.getAddresses(instance = nil, id: nil, region: MU.curRegion, credentials: nil)
  return nil if !instance and !id

  instance ||= find(cloud_id: id, region: region, credentials: credentials).values.first
  return if !instance

  ips = []
  names = []
  instance.network_interfaces.each { |iface|
    iface.private_ip_addresses.each { |ip|
      ips << ip.private_ip_address
      names << ip.private_dns_name
      if ip.association
        ips << ip.association.public_ip
        names << ip.association.public_dns_name
      end
    }
  }

  [ips, names]
end
imageTimeStamp(ami_id, credentials: nil, region: nil) click to toggle source

Return the date/time a machine image was created. @param ami_id [String]: AMI identifier of an Amazon Machine Image @param credentials [String] @return [DateTime]

# File modules/mu/providers/aws/server.rb, line 1913
def self.imageTimeStamp(ami_id, credentials: nil, region: nil)
  begin
    img = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_images(image_ids: [ami_id]).images.first
    return DateTime.new if img.nil?
    return DateTime.parse(img.creation_date)
  rescue Aws::EC2::Errors::InvalidAMIIDNotFound
  end

  return DateTime.new
end
isGlobal?() click to toggle source

Does this resource type exist as a global (cloud-wide) artifact, or is it localized to a region/zone? @return [Boolean]

# File modules/mu/providers/aws/server.rb, line 1474
def self.isGlobal?
  false
end
new(**args) click to toggle source

Initialize this cloud resource object. Calling super will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us. @param args [Hash]: Hash of named arguments passed via Ruby's double-splat

Calls superclass method
# File modules/mu/providers/aws/server.rb, line 80
  def initialize(**args)
    super
    @userdata = if @config['userdata_script']
      @config['userdata_script']
    elsif @deploy and !@config['scrub_mu_isms']
      MU::Cloud.fetchUserdata(
        platform: @config["platform"],
        cloud: "AWS",
        credentials: @credentials,
        template_variables: {
          "deployKey" => Base64.urlsafe_encode64(@deploy.public_key),
          "deploySSHKey" => @deploy.ssh_public_key,
          "muID" => @deploy.deploy_id,
          "muUser" => MU.mu_user,
          "publicIP" => MU.mu_public_ip,
          "mommaCatPort" => MU.mommaCatPort,
          "adminBucketName" => MU::Cloud::AWS.adminBucketName(@credentials),
          "chefVersion" => MU.chefVersion,
          "skipApplyUpdates" => @config['skipinitialupdates'],
        "windowsAdminName" => @config['windows_admin_username'],
        "resourceName" => @config["name"],
        "resourceType" => "server",
        "platform" => @config["platform"]
      },
      custom_append: @config['userdata_script']
    )
  end

  @disk_devices = MU::Cloud::AWS::Server.disk_devices
  @ephemeral_mappings = MU::Cloud::AWS::Server.ephemeral_mappings

  if !@mu_name.nil?
    @config['mu_name'] = @mu_name
    @mu_windows_name = @deploydata['mu_windows_name'] if @mu_windows_name.nil? and @deploydata
  else
    if kitten_cfg.has_key?("basis")
      @mu_name = @deploy.getResourceName(@config['name'], need_unique_string: true)
    else
      @mu_name = @deploy.getResourceName(@config['name'])
    end
    @config['mu_name'] = @mu_name

  end

  @config['instance_secret'] ||= Password.random(50)

  @groomer = MU::Groomer.new(self) unless MU.inGem?
end
quality() click to toggle source

Denote whether this resource implementation is experiment, ready for testing, or ready for production use.

# File modules/mu/providers/aws/server.rb, line 1480
def self.quality
  MU::Cloud::RELEASE
end
schema(_config) click to toggle source

Cloud-specific configuration properties. @param _config [MU::Config]: The calling MU::Config object @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource

# File modules/mu/providers/aws/server.rb, line 1677
def self.schema(_config)
  toplevel_required = []
  schema = {
    "ami_id" => {
      "type" => "string",
      "description" => "Alias for +image_id+"
    },
    "windows_admin_username" => {
      "type" => "string",
      "default" => "Administrator"
    },
    "generate_iam_role" => {
      "type" => "boolean",
      "default" => true,
      "description" => "Generate a unique IAM profile for this Server or ServerPool.",
    },
    "iam_role" => {
      "type" => "string",
      "description" => "An Amazon IAM instance profile, from which to harvest role policies to merge into this node's own instance profile. If generate_iam_role is false, will simple use this profile.",
    },
    "canned_iam_policies" => {
      "type" => "array",
      "items" => {
        "description" => "IAM policies to attach, pre-defined by Amazon (e.g. AmazonEKSWorkerNodePolicy)",
        "type" => "string"
      }
    },
    "iam_policies" => {
      "type" => "array",
      "items" => {
        "description" => "Amazon-compatible role policies which will be merged into this node's own instance profile.  Not valid with generate_iam_role set to false. Our parser expects the role policy document to me embedded under a named container, e.g. { 'name_of_policy':'{ <policy document> } }",
        "type" => "object"
      }
    },
    "ingress_rules" => MU::Cloud.resourceClass("AWS", "FirewallRule").ingressRuleAddtlSchema,
    "ssh_user" => {
      "type" => "string",
      "default" => "root",
      "default_if" => [
        {
          "key_is" => "platform",
          "value_is" => "windows",
          "set" => "Administrator"
        },
        {
          "key_is" => "platform",
          "value_is" => "win2k12",
          "set" => "Administrator"
        },
        {
          "key_is" => "platform",
          "value_is" => "win2k12r2",
          "set" => "Administrator"
        },
        {
          "key_is" => "platform",
          "value_is" => "win2k16",
          "set" => "Administrator"
        },
        {
          "key_is" => "platform",
          "value_is" => "rhel7",
          "set" => "ec2-user"
        },
        {
          "key_is" => "platform",
          "value_is" => "rhel71",
          "set" => "ec2-user"
        },
        {
          "key_is" => "platform",
          "value_is" => "amazon",
          "set" => "ec2-user"
        }
      ]
    }
  }
  [toplevel_required, schema]
end
tagVolumes(instance_id, device: nil, tag_name: "MU-ID", tag_value: MU.deploy_id, region: MU.curRegion, credentials: nil) click to toggle source

Find volumes attached to a given instance id and tag them. If no arguments besides the instance id are provided, it will add our special MU-ID tag. Can also be used to do things like set the resource's name, if you leverage the other arguments. @param instance_id [String]: The cloud provider's identifier for the parent instance of this volume. @param device [String]: The OS-level device name of the volume. @param tag_name [String]: The name of the tag to attach. @param tag_value [String]: The value of the tag to attach. @param region [String]: The cloud provider region @return [void]

# File modules/mu/providers/aws/server.rb, line 207
def self.tagVolumes(instance_id, device: nil, tag_name: "MU-ID", tag_value: MU.deploy_id, region: MU.curRegion, credentials: nil)
  MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_volumes(filters: [name: "attachment.instance-id", values: [instance_id]]).each { |vol|
    vol.volumes.each { |volume|
      volume.attachments.each { |attachment|
        vol_parent = attachment.instance_id
        vol_id = attachment.volume_id
        vol_dev = attachment.device
        if vol_parent == instance_id and (vol_dev == device or device.nil?)
          MU::Cloud::AWS.createTag(vol_id, tag_name, tag_value, region: region, credentials: credentials)
          break
        end
      }
    }
  }
end
terminateInstance(instance: nil, noop: false, id: nil, onlycloud: false, region: MU.curRegion, deploy_id: MU.deploy_id, mu_name: nil, credentials: nil) click to toggle source

Terminate an instance. @param instance [OpenStruct]: The cloud provider's description of the instance. @param id [String]: The cloud provider's identifier for the instance, to use if the full description is not available. @param region [String]: The cloud provider region @return [void]

# File modules/mu/providers/aws/server.rb, line 1581
def self.terminateInstance(instance: nil, noop: false, id: nil, onlycloud: false, region: MU.curRegion, deploy_id: MU.deploy_id, mu_name: nil, credentials: nil)
  if !id and !instance
    MU.log "You must supply an instance handle or id to terminateInstance", MU::ERR
    return
  end
  instance ||= find(cloud_id: id, region: region, credentials: credentials).values.first
  return if !instance

  id ||= instance.instance_id
  begin
    MU::MommaCat.lock(".cleanup-"+id)
  rescue Errno::ENOENT => e
    MU.log "No lock for terminating instance #{id} due to missing metadata", MU::DEBUG
  end

  ips, names = getAddresses(instance, region: region, credentials: credentials)
  targets = ips +names

  server_obj = MU::MommaCat.findStray(
    "AWS",
    "servers",
    region: region,
    deploy_id: deploy_id,
    cloud_id: id,
    mu_name: mu_name,
    dummy_ok: true
  ).first

  if MU::Cloud::AWS.hosted? and !MU::Cloud::AWS.isGovCloud? and server_obj
    targets.each { |target|
      MU::Cloud::DNSZone.genericMuDNSEntry(name: server_obj.mu_name, target: target, cloudclass: MU::Cloud::Server, delete: true, noop: noop)
    }
  end

  if targets.size > 0 and !onlycloud
    MU::Master.removeInstanceFromEtcHosts(server_obj.mu_name) if !noop and server_obj
    targets.each { |target|
      next if !target.match(/^\d+\.\d+\.\d+\.\d+$/)
      MU::Master.removeIPFromSSHKnownHosts(target, noop: noop)
    }
  end

  on_retry = Proc.new {
    instance = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_instances(instance_ids: [instance.instance_id]).reservations.first.instances.first
    if instance.state.name == "terminated"
      MU.log "#{instance.instance_id}#{server_obj ? " ("+server_obj.mu_name+")" : ""} has already been terminated, skipping"
      MU::MommaCat.unlock(".cleanup-"+id)
      return
    end
  }

  loop_if = Proc.new {
    instance = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_instances(instance_ids: [instance.instance_id]).reservations.first.instances.first
    instance.state.name != "terminated"
  }

  MU.log "Terminating #{instance.instance_id}#{server_obj ? " ("+server_obj.mu_name+")" : ""}"
  if !noop
    MU.retrier([Aws::EC2::Errors::IncorrectInstanceState, Aws::EC2::Errors::InternalError], wait: 30, max: 60, loop_if: loop_if, on_retry: on_retry) {
      MU::Cloud::AWS.ec2(credentials: credentials, region: region).modify_instance_attribute(
        instance_id: instance.instance_id,
        disable_api_termination: {value: false}
      )
      MU::Cloud::AWS.ec2(credentials: credentials, region: region).terminate_instances(instance_ids: [instance.instance_id])
    }
  end

  MU.log "#{instance.instance_id}#{server_obj ? " ("+server_obj.mu_name+")" : ""} terminated" if !noop
  begin
    MU::MommaCat.unlock(".cleanup-"+id)
  rescue Errno::ENOENT => e
    MU.log "No lock for terminating instance #{id} due to missing metadata", MU::DEBUG
  end

end
validateConfig(server, configurator) click to toggle source

Cloud-specific pre-processing of {MU::Config::BasketofKittens::servers}, bare and unvalidated. @param server [Hash]: The resource to process and validate @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member @return [Boolean]: True if validation succeeded, False otherwise

# File modules/mu/providers/aws/server.rb, line 1856
def self.validateConfig(server, configurator)
  ok = true

  server['size'] = validateInstanceType(server["size"], server["region"])
  ok = false if server['size'].nil?

  if !server['generate_iam_role']
    if !server['iam_role'] and server['cloud'] != "CloudFormation"
      MU.log "Must set iam_role if generate_iam_role set to false", MU::ERR
      ok = false
    end
    if !server['iam_policies'].nil? and server['iam_policies'].size > 0
      MU.log "Cannot mix iam_policies with generate_iam_role set to false", MU::ERR
      ok = false
    end
  end

  generateStandardRole(server, configurator)

  if !server['create_image'].nil?
    if server['create_image'].has_key?('copy_to_regions') and
        (server['create_image']['copy_to_regions'].nil? or
            server['create_image']['copy_to_regions'].include?("#ALL") or
            server['create_image']['copy_to_regions'].size == 0
        )
      server['create_image']['copy_to_regions'] = MU::Cloud::AWS.listRegions(server['us_only'])
    end
  end

  server['image_id'] ||= server['ami_id']

  if server['image_id'].nil?
    img_id = MU::Cloud.getStockImage("AWS", platform: server['platform'], region: server['region'])
    if img_id
      server['image_id'] = configurator.getTail("server"+server['name']+"AMI", value: img_id, prettyname: "server"+server['name']+"AMI", cloudtype: "AWS::EC2::Image::Id")
    else
      MU.log "No AMI specified for #{server['name']} and no default available for platform #{server['platform']} in region #{server['region']}", MU::ERR, details: server
      ok = false
    end
  end

  if !server["loadbalancers"].nil?
    server["loadbalancers"].each { |lb|
      lb["name"] ||= lb["concurrent_load_balancer"]
      if lb["name"]
        MU::Config.addDependency(server, lb["name"], "loadbalancer")
      end
    }
  end

  ok
end
validateInstanceType(size, region) click to toggle source

Confirm that the given instance size is valid for the given region. If someone accidentally specified an equivalent size from some other cloud provider, return something that makes sense. If nothing makes sense, return nil. @param size [String]: Instance type to check @param region [String]: Region to check against @return [String,nil]

# File modules/mu/providers/aws/server.rb, line 1762
def self.validateInstanceType(size, region)
  size = size.dup.to_s
  types = begin
    (MU::Cloud::AWS.listInstanceTypes(region))[region]
  rescue Aws::Pricing::Errors::Unrecognitypes.has_key?(size)
    MU.log "Saw authentication error communicating with Pricing API, going to assume our instance type is correct", MU::WARN
    return size
  end


  return size if types.has_key?(size)

  if size.nil? or !types.has_key?(size)
    # See if it's a type we can approximate from one of the other clouds
    foundmatch = false

    MU::Cloud.availableClouds.each { |cloud|
      next if cloud == "AWS"
      foreign_types = (MU::Cloud.cloudClass(cloud).listInstanceTypes).values.first
      if foreign_types.size == 1
        foreign_types = foreign_types.values.first
      end
      if foreign_types and foreign_types.size > 0 and foreign_types.has_key?(size)
        vcpu = foreign_types[size]["vcpu"]
        mem = foreign_types[size]["memory"]
        ecu = foreign_types[size]["ecu"]
        types.keys.sort.reverse.each { |type|
          features = types[type]
          next if ecu == "Variable" and ecu != features["ecu"]
          next if features["vcpu"] != vcpu
          if (features["memory"] - mem.to_f).abs < 0.10*mem
            foundmatch = true
            MU.log "You specified #{cloud} instance type '#{size}.' Approximating with Amazon EC2 type '#{type}.'", MU::WARN
            size = type
            break
          end
        }
      end
      break if foundmatch
    }

    if !foundmatch
      MU.log "Invalid size '#{size}' for AWS EC2 instance in #{region}. Supported types:", MU::ERR, details: types.keys.sort.join(", ")
      return nil
    end
  end
  size
end
waitForAMI(image_id, region: MU.curRegion, credentials: nil) click to toggle source

Given a cloud platform identifier for a machine image, wait until it's flagged as ready. @param image_id [String]: The machine image to wait for. @param region [String]: The cloud provider region

# File modules/mu/providers/aws/server.rb, line 1094
def self.waitForAMI(image_id, region: MU.curRegion, credentials: nil)
  MU.log "Checking to see if AMI #{image_id} is available", MU::DEBUG

  retries = 0
  begin
    images = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_images(image_ids: [image_id]).images
    if images.nil? or images.size == 0
      raise MuError, "No such AMI #{image_id} found"
    end
    state = images.first.state
    if state == "failed"
      raise MuError, "#{image_id} is marked as failed! I can't use this."
    end
    if state != "available"
      loglevel = MU::DEBUG
      loglevel = MU::NOTICE if retries % 3 == 0
      MU.log "Waiting for AMI #{image_id} in #{region} (#{state})", loglevel
      sleep 60
    end
  rescue Aws::EC2::Errors::InvalidAMIIDNotFound => e
    retries = retries + 1
    if retries >= 10
      raise e
    end
    sleep 5
    retry
  end while state != "available"
  MU.log "AMI #{image_id} is ready", MU::DEBUG
end

Private Class Methods

delete_volume(volume, noop, skipsnapshots, region: MU.curRegion, credentials: nil, deploy_id: MU.deploy_id) click to toggle source

Destroy a volume. @param volume [OpenStruct]: The cloud provider's description of the volume. @param region [String]: The cloud provider region @return [void]

# File modules/mu/providers/aws/server.rb, line 1928
def self.delete_volume(volume, noop, skipsnapshots, region: MU.curRegion, credentials: nil, deploy_id: MU.deploy_id)
  if !volume.nil?
    resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_volumes(volume_ids: [volume.volume_id])
    volume = resp.data.volumes.first
  end
  name = nil
  volume.tags.each { |tag|
    name = tag.value if tag.key == "Name"
  }
  name ||= volume.volume_id

  MU.log("Deleting volume #{volume.volume_id} (#{name})")
  if !noop
    if !skipsnapshots
      if !name.nil? and !name.empty?
        desc = "#{deploy_id}-MUfinal (#{name})"
      else
        desc = "#{deploy_id}-MUfinal"
      end

      begin
        MU::Cloud::AWS.ec2(region: region, credentials: credentials).create_snapshot(
          volume_id: volume.volume_id,
          description: desc
        )
      rescue Aws::EC2::Errors::IncorrectState => e
        if e.message.match(/'deleting'/)
          MU.log "Cannot snapshot volume '#{name}', is already being deleted", MU::WARN
        end
      end
    end

    begin
      MU.retrier([Aws::EC2::Errors::IncorrectState, Aws::EC2::Errors::VolumeInUse], ignoreme: [Aws::EC2::Errors::InvalidVolumeNotFound], wait: 30, max: 10){
        MU::Cloud::AWS.ec2(region: region, credentials: credentials).delete_volume(volume_id: volume.volume_id)
      }
    rescue Aws::EC2::Errors::VolumeInUse
      MU.log "Failed to delete #{name}", MU::ERR
    end

  end
end
getIAMProfile(myname, deploy, generated: true, role_name: nil, region: nil, credentials: nil, want_arn: false) click to toggle source

XXX move to public section

# File modules/mu/providers/aws/server.rb, line 2284
def self.getIAMProfile(myname, deploy, generated: true, role_name: nil, region: nil, credentials: nil, want_arn: false)

  arn = if generated
    role = deploy.findLitterMate(name: myname, type: "roles", debug: true)
    if !role
      raise MuError, "Failed to find a role matching #{myname}"
    end
    s3_objs = ["#{deploy.deploy_id}-secret", "#{role.mu_name}.pfx", "#{role.mu_name}.crt", "#{role.mu_name}.key", "#{role.mu_name}-winrm.crt", "#{role.mu_name}-winrm.key"].map { |file| 
      'arn:'+(MU::Cloud::AWS.isGovCloud?(region) ? "aws-us-gov" : "aws")+':s3:::'+MU::Cloud::AWS.adminBucketName(credentials)+'/'+file
    }
    MU.log "Adding S3 read permissions to #{myname}'s IAM profile", MU::NOTICE, details: s3_objs
    role.cloudobj.injectPolicyTargets("MuSecrets", s3_objs)
  
    role_name = role.mu_name
    role.cloudobj.createInstanceProfile
  
  elsif role_name.nil?
    raise MuError, "#{myname} has generate_iam_role set to false, but no iam_role assigned."
  else
    begin
      ext_prof = MU::Cloud::AWS.iam(credentials: credentials).get_instance_profile(instance_profile_name: role_name)
      role_name = ext_prof.instance_profile.instance_profile_name
      ext_prof.instance_profile.arn
    rescue Aws::IAM::Errors::NoSuchEntity
      role = MU::MommaCat.findStray("AWS", "role", cloud_id: role_name, dummy_ok: true, credentials: credentials).first
      if !role
        raise MuError, "#{myname} specified iam_role '#{role_name}', but I can't find a role with that name to use when creating an instance profile"
      end
      role.cloudobj.createInstanceProfile
    end
  end

  role_or_policy = deploy.findLitterMate(name: myname, type: "roles")

  # Make sure our permissions to read our identity secrets are set
  s3_objs = [
    "#{deploy.deploy_id}-secret",
    "#{role_or_policy.mu_name}.pfx",
    "#{role_or_policy.mu_name}.crt",
    "#{role_or_policy.mu_name}.key",
    "#{role_or_policy.mu_name}-winrm.crt",
    "#{role_or_policy.mu_name}-winrm.key"].map { |file| 
      'arn:'+(MU::Cloud::AWS.isGovCloud?(region) ? "aws-us-gov" : "aws")+':s3:::'+MU::Cloud::AWS.adminBucketName(credentials)+'/'+file
    }
  if generated
    role_or_policy.injectPolicyTargets("MuSecrets", s3_objs)
  elsif role_name
    realrole = MU::MommaCat.findStray("AWS", "role", cloud_id: role_name, dummy_ok: true, credentials: credentials).first
    if !role_or_policy
      raise MuError, "I should have a bare policy littermate named #{name} but I can't find it"
    end
    if realrole
      role_or_policy.bindTo("role", realrole.cloud_id)
      realrole.injectPolicyTargets(role_or_policy.mu_name+"-MUSECRETS", s3_objs)
    end
  end

  if !role_name.nil?
    if arn and want_arn
      return {arn: arn}
    else
      return {name: role_name}
    end
  end

  nil
end

Public Instance Methods

active?() click to toggle source

Determine whether the node in question exists at the Cloud provider layer. @return [Boolean]

# File modules/mu/providers/aws/server.rb, line 1362
def active?
  if @cloud_id.nil? or @cloud_id.empty?
    MU.log "#{self} didn't have a #{@cloud_id}, couldn't determine 'active?' status", MU::ERR
    return true
  end
  begin
    MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_instances(
        instance_ids: [@cloud_id]
    ).reservations.each { |resp|
      if !resp.nil? and !resp.instances.nil?
        resp.instances.each { |instance|
          if instance.state.name == "terminated" or
              instance.state.name == "terminating"
            return false
          end
          return true
        }
      end
    }
  rescue Aws::EC2::Errors::InvalidInstanceIDNotFound
    return false
  end
  return false
end
addVolume(dev, size, type: "gp2", delete_on_termination: false) click to toggle source

Add a volume to this instance @param dev [String]: Device name to use when attaching to instance @param size [String]: Size (in gb) of the new volume @param type [String]: Cloud storage type of the volume, if applicable @param delete_on_termination [Boolean]: Value of delete_on_termination flag to set

# File modules/mu/providers/aws/server.rb, line 1298
def addVolume(dev, size, type: "gp2", delete_on_termination: false)

  if setDeleteOntermination(dev, delete_on_termination)
    MU.log "A volume #{dev} already attached to #{self}, skipping", MU::NOTICE
    return
  end

  MU.log "Creating #{size}GB #{type} volume on #{dev} for #{@cloud_id}"
  creation = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).create_volume(
    availability_zone: cloud_desc.placement.availability_zone,
    size: size,
    volume_type: type
  )

  MU.retrier(wait: 3, loop_if: Proc.new {
    creation = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_volumes(volume_ids: [creation.volume_id]).volumes.first
    if !["creating", "available"].include?(creation.state)
      raise MuError, "Saw state '#{creation.state}' while creating #{size}GB #{type} volume on #{dev} for #{@cloud_id}"
    end
    creation.state != "available"
  })


  if @deploy
    MU::Cloud::AWS.createStandardTags(
      creation.volume_id,
      region: @region,
      credentials: @credentials,
      optional: @config['optional_tags'],
      nametag: @mu_name+"-"+dev.upcase,
      othertags: @config['tags']
    )
  end

  MU.log "Attaching #{creation.volume_id} as #{dev} to #{@cloud_id} in #{@region} (credentials #{@credentials})"
  attachment = nil
  MU.retrier([Aws::EC2::Errors::IncorrectState], wait: 15, max: 4) {
    attachment = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).attach_volume(
      device: dev,
      instance_id: @cloud_id,
      volume_id: creation.volume_id
    )
  }

  begin
    att_resp = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_volumes(volume_ids: [attachment.volume_id])
    if att_resp and att_resp.volumes and !att_resp.volumes.empty? and
       att_resp.volumes.first.attachments and
       !att_resp.volumes.first.attachments.empty?
      attachment = att_resp.volumes.first.attachments.first
      if !attachment.nil? and !["attaching", "attached"].include?(attachment.state)
        raise MuError, "Saw state '#{creation.state}' while creating #{size}GB #{type} volume on #{dev} for #{@cloud_id}"
      end
    end
  end while attachment.nil? or attachment.state != "attached"

  # Set delete_on_termination, which for some reason is an instance
  # attribute and not on the attachment
  setDeleteOntermination(dev, delete_on_termination)
end
arn() click to toggle source

Canonical Amazon Resource Number for this resource @return [String]

# File modules/mu/providers/aws/server.rb, line 911
def arn
  "arn:"+(MU::Cloud::AWS.isGovCloud?(@region) ? "aws-us-gov" : "aws")+":ec2:"+@region+":"+MU::Cloud::AWS.credToAcct(@credentials)+":instance/"+@cloud_id
end
canonicalIP() click to toggle source

Return the IP address that we, the Mu server, should be using to access this host via the network. Note that this does not factor in SSH bastion hosts that may be in the path, see getSSHConfig if that's what you need.

# File modules/mu/providers/aws/server.rb, line 951
def canonicalIP
  if !cloud_desc
    raise MuError, "Couldn't retrieve cloud descriptor for server #{self}"
  end

  if deploydata.nil? or
      (!deploydata.has_key?("private_ip_address") and
          !deploydata.has_key?("public_ip_address"))
    return nil if cloud_desc.nil?
    @deploydata = {} if @deploydata.nil?
    @deploydata["public_ip_address"] = cloud_desc.public_ip_address
    @deploydata["public_dns_name"] = cloud_desc.public_dns_name
    @deploydata["private_ip_address"] = cloud_desc.private_ip_address
    @deploydata["private_dns_name"] = cloud_desc.private_dns_name

    notify
  end

  # Our deploydata gets corrupted often with server pools, this will cause us to use the wrong IP to identify a node
  # which will cause us to create certificates, DNS records and other artifacts with incorrect information which will cause our deploy to fail.
  # The cloud_id is always correct so lets use 'cloud_desc' to get the correct IPs
  if MU::Cloud.resourceClass("AWS", "VPC").haveRouteToInstance?(cloud_desc, region: @region, credentials: @credentials) or @deploydata["public_ip_address"].nil?
    @config['canonical_ip'] = cloud_desc.private_ip_address
    @deploydata["private_ip_address"] = cloud_desc.private_ip_address
    return cloud_desc.private_ip_address
  else
    @config['canonical_ip'] = cloud_desc.public_ip_address
    @deploydata["public_ip_address"] = cloud_desc.public_ip_address
    return cloud_desc.public_ip_address
  end
end
cloud_desc(use_cache: true) click to toggle source

Return the cloud provider's description for this instance @return [Openstruct]

# File modules/mu/providers/aws/server.rb, line 918
def cloud_desc(use_cache: true)
  return @cloud_desc_cache if @cloud_desc_cache and use_cache
  return nil if !@cloud_id
  max_retries = 5
  retries = 0
  if !@cloud_id.nil?
    begin
      resp = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_instances(instance_ids: [@cloud_id])
      if resp and resp.reservations and resp.reservations.first and
         resp.reservations.first.instances and
         resp.reservations.first.instances.first
        @cloud_desc_cache = resp.reservations.first.instances.first
        return @cloud_desc_cache
      end
    rescue Aws::EC2::Errors::InvalidInstanceIDNotFound
      return nil
    rescue NoMethodError
      if retries >= max_retries
        raise MuError, "Couldn't get a cloud descriptor for #{@mu_name} (#{@cloud_id})"
      else
        retries = retries + 1
        sleep 10
        retry
      end
    end
  end
  nil
end
create() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/aws/server.rb, line 224
def create
  begin
    done = false
    instance = createEc2Instance

    @cloud_id = instance.instance_id
    @deploy.saveNodeSecret(@cloud_id, @config['instance_secret'], "instance_secret")
    @config.delete("instance_secret")

    if !@config['async_groom']
      sleep 5
      MU::MommaCat.lock(instance.instance_id+"-create")
      if !postBoot
        MU.log "#{@config['name']} is already being groomed, skipping", MU::NOTICE
      else
        MU.log "Node creation complete for #{@config['name']}"
      end
      MU::MommaCat.unlock(instance.instance_id+"-create")
    else
      MU::Cloud::AWS.createStandardTags(
        instance.instance_id,
        region: @region,
        credentials: @credentials,
        optional: @config['optional_tags'],
        nametag: @mu_name,
        othertags: @config['tags']
      )
    end
    done = true
  rescue StandardError => e
    if !instance.nil? and !done
      MU.log "Aborted before I could finish setting up #{@config['name']}, cleaning it up. Stack trace will print once cleanup is complete.", MU::WARN if !@deploy.nocleanup
      MU::MommaCat.unlockAll
      if !@deploy.nocleanup
        parent_thread_id = Thread.current.object_id
        Thread.new {
          MU.dupGlobals(parent_thread_id)
          MU::Cloud::AWS::Server.cleanup(noop: false, ignoremaster: false, region: @region, credentials: @credentials, flags: { "skipsnapshots" => true } )
        }
      end
    end
    raise e
  end

  return @config
end
createEc2Instance() click to toggle source

Create an Amazon EC2 instance.

# File modules/mu/providers/aws/server.rb, line 272
def createEc2Instance

  instance_descriptor = {
    :image_id => @config["image_id"],
    :key_name => @deploy.ssh_key_name,
    :instance_type => @config["size"],
    :disable_api_termination => true,
    :min_count => 1,
    :max_count => 1
  }

  instance_descriptor[:iam_instance_profile] = getIAMProfile

  security_groups = myFirewallRules.map { |fw| fw.cloud_id }
  if security_groups.size > 0
    instance_descriptor[:security_group_ids] = security_groups
  else
    raise MuError, "Didn't get any security groups assigned to be in #{@mu_name}, that shouldn't happen"
  end

  if @config['private_ip']
    instance_descriptor[:private_ip_address] = @config['private_ip']
  end

  if !@vpc.nil? and @config.has_key?("vpc")
    subnet = mySubnets.sample
    if subnet.nil?
      raise MuError, "Got null subnet id out of #{@config['vpc']}"
    end
    MU.log "Deploying #{@mu_name} into VPC #{@vpc.cloud_id} Subnet #{subnet.cloud_id}"
    allowBastionAccess
    instance_descriptor[:subnet_id] = subnet.cloud_id
  end

  if !@userdata.nil? and !@userdata.empty?
    instance_descriptor[:user_data] = Base64.encode64(@userdata)
  end

  MU::Cloud::AWS::Server.waitForAMI(@config["image_id"], region: @region, credentials: @credentials)

  instance_descriptor[:block_device_mappings] = MU::Cloud::AWS::Server.configureBlockDevices(image_id: @config["image_id"], storage: @config['storage'], region: @region, credentials: @credentials)

  instance_descriptor[:monitoring] = {enabled: @config['monitoring']}

  if @tags and @tags.size > 0
    instance_descriptor[:tag_specifications] = [{
      :resource_type => "instance",
      :tags => @tags.keys.map { |k|
        { :key => k, :value => @tags[k] }
      }
    }]
  end

  MU.log "Creating EC2 instance #{@mu_name}", details: instance_descriptor

  instance = resp = nil
  loop_if = Proc.new {
    instance = resp.instances.first if resp and resp.instances
    resp.nil? or resp.instances.nil? or instance.nil?
  }

  bad_subnets = []
  mysubnet_ids = if mySubnets
    mySubnets.map { |s| s.cloud_id }
  end
  begin
    MU.retrier([Aws::EC2::Errors::InvalidGroupNotFound, Aws::EC2::Errors::InvalidSubnetIDNotFound, Aws::EC2::Errors::InvalidParameterValue], loop_if: loop_if, loop_msg: "Waiting for run_instances to return #{@mu_name}") {
      resp = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).run_instances(instance_descriptor)
    }
  rescue Aws::EC2::Errors::Unsupported => e
    bad_subnets << instance_descriptor[:subnet_id]
    better_subnet = (mysubnet_ids - bad_subnets).sample
    if e.message !~ /is not supported in your requested Availability Zone/ and
       (mysubnet_ids.nil? or mysubnet_ids.empty? or
        mysubnet_ids.size == bad_subnets.size or
        better_subnet.nil? or better_subnet == "")
      raise MuError.new e.message, details: mysubnet_ids
    end
    instance_descriptor[:subnet_id] = (mysubnet_ids - bad_subnets).sample
    MU.log "One or more subnets does not support this instance type, attempting with #{instance_descriptor[:subnet_id]} instead", MU::WARN, details: bad_subnets
    retry
  rescue Aws::EC2::Errors::InvalidRequest => e
    MU.log e.message, MU::ERR, details: instance_descriptor
    raise e
  end

  MU.log "#{@mu_name} (#{instance.instance_id}) coming online"

  instance
end
getSSHConfig() click to toggle source

Figure out what's needed to SSH into this server. @return [Array<String>]: nat_ssh_key, nat_ssh_user, nat_ssh_host, canonical_ip, ssh_user, ssh_key_name, alternate_names

# File modules/mu/providers/aws/server.rb, line 417
      def getSSHConfig
        cloud_desc(use_cache: false) # make sure we're current
# XXX add some awesome alternate names from metadata and make sure they end
# up in MU::MommaCat's ssh config wangling
        return nil if @config.nil? or @deploy.nil?

        nat_ssh_key = nat_ssh_user = nat_ssh_host = nil
        if !@config["vpc"].nil? and !MU::Cloud.resourceClass("AWS", "VPC").haveRouteToInstance?(cloud_desc, region: @region, credentials: @credentials)
          if !@nat.nil?
            if @nat.is_a?(Struct) && @nat.nat_gateway_id && @nat.nat_gateway_id.start_with?("nat-")
              raise MuError, "Configured to use NAT Gateway, but I have no route to instance. Either use Bastion, or configure VPC peering"
            end

            if @nat.cloud_desc.nil?
              MU.log "NAT was missing cloud descriptor when called in #{@mu_name}'s getSSHConfig", MU::ERR
              return nil
            end
            # XXX Yanking these things from the cloud descriptor will only work in AWS!

            nat_ssh_key = @nat.cloud_desc.key_name
                                                        nat_ssh_key = @config["vpc"]["nat_ssh_key"] if !@config["vpc"]["nat_ssh_key"].nil?
            nat_ssh_host = @nat.cloud_desc.public_ip_address
            nat_ssh_user = @config["vpc"]["nat_ssh_user"]
            if nat_ssh_user.nil? and !nat_ssh_host.nil?
              MU.log "#{@config["name"]} (#{MU.deploy_id}) is configured to use #{@config['vpc']} NAT #{nat_ssh_host}, but username isn't specified. Guessing root.", MU::ERR, details: caller
              nat_ssh_user = "root"
            end
          end
        end

        if @config['ssh_user'].nil?
          if windows?
            @config['ssh_user'] = "Administrator"
          else
            @config['ssh_user'] = "root"
          end
        end

        return [nat_ssh_key, nat_ssh_user, nat_ssh_host, canonicalIP, @config['ssh_user'], @deploy.ssh_key_name]

      end
getWindowsAdminPassword(use_cache: true) click to toggle source

Retrieves the Cloud provider's randomly generated Windows password Will only work on stock Amazon Windows AMIs or custom AMIs that where created with Administrator Password set to random in EC2Config return [String]: A password string.

# File modules/mu/providers/aws/server.rb, line 1172
def getWindowsAdminPassword(use_cache: true)
  @config['windows_auth_vault'] ||= {
    "vault" => @mu_name,
    "item" => "windows_credentials",
    "password_field" => "password"
  }

  if use_cache
    begin
      win_admin_password = @groomer.getSecret(
        vault: @config['windows_auth_vault']['vault'],
        item: @config['windows_auth_vault']['item'],
        field: @config["windows_auth_vault"]["password_field"]
      )

      return win_admin_password if win_admin_password
    rescue MU::Groomer::MuNoSuchSecret, MU::Groomer::RunError
    end
  end

  @cloud_id ||= cloud_desc(use_cache: false).instance_id
  ssh_keydir = "#{Etc.getpwuid(Process.uid).dir}/.ssh"
  ssh_key_name = @deploy.ssh_key_name

  retries = 0
  MU.log "Waiting for Windows instance password to be set by Amazon and flagged as available from the API. Note- if you're using a source AMI that already has its password set, this may fail. You'll want to set use_cloud_provider_windows_password to false if this is the case.", MU::NOTICE
  begin
    MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).wait_until(:password_data_available, instance_id: @cloud_id) do |waiter|
      waiter.max_attempts = 60
      waiter.before_attempt do |attempts|
        MU.log "Waiting for Windows password data to be available for node #{@mu_name}", MU::NOTICE if attempts % 5 == 0
      end
      # waiter.before_wait do |attempts, resp|
      # throw :success if resp.data.password_data and !resp.data.password_data.empty?
      # end
    end
  rescue Aws::Waiters::Errors::TooManyAttemptsError => e
    if retries < 2
      retries = retries + 1
      MU.log "wait_until(:password_data_available, instance_id: #{@cloud_id}) in #{@region} never got a good response, retrying (#{retries}/2)", MU::WARN, details: e.inspect
      retry
    else
      MU.log "wait_until(:password_data_available, instance_id: #{@cloud_id}) in #{@region} never returned- this image may not be configured to have its password set by AWS.", MU::ERR
      return nil
    end
  end

  resp = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).get_password_data(instance_id: @cloud_id)
  encrypted_password = resp.password_data

  # Note: This is already implemented in the decrypt_windows_password API call
  decoded = Base64.decode64(encrypted_password)
  pem_bytes = File.open("#{ssh_keydir}/#{ssh_key_name}", 'rb') { |f| f.read }
  private_key = OpenSSL::PKey::RSA.new(pem_bytes)
  decrypted_password = private_key.private_decrypt(decoded)
  saveCredentials(decrypted_password)

  return decrypted_password
end
groom() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/aws/server.rb, line 838
def groom
  MU::MommaCat.lock(@cloud_id+"-groom")

  # Make double sure we don't lose a cached mu_windows_name value.
  if windows? or !@config['active_directory'].nil?
    if @mu_windows_name.nil?
      @mu_windows_name = deploydata['mu_windows_name']
    end
  end

  allowBastionAccess

  tagVolumes

  # If we have a loadbalancer configured, attach us to it
  if !@config['loadbalancers'].nil?
    if @loadbalancers.nil?
      raise MuError, "#{@mu_name} is configured to use LoadBalancers, but none have been loaded by dependencies()"
    end
    @loadbalancers.each { |lb|
      lb.registerNode(@cloud_id)
    }
  end
  MU.log %Q{Server #{@config['name']} private IP is #{@deploydata["private_ip_address"]}#{@deploydata["public_ip_address"] ? ", public IP is "+@deploydata["public_ip_address"] : ""}}, MU::SUMMARY

  # Let us into any databases we depend on.
  # This is probelmtic with autscaling - old ips are not removed, and access to the database can easily be given at the BoK level
  # if @dependencies.has_key?("database")
    # @dependencies['database'].values.each { |db|
      # db.allowHost(@deploydata["private_ip_address"]+"/32")
      # if @deploydata["public_ip_address"]
        # db.allowHost(@deploydata["public_ip_address"]+"/32")
      # end
    # }
  # end

  if @config['groom'].nil? or @config['groom']
    @groomer.saveDeployData
  end

  begin
    getIAMProfile

    dbs = @deploy.findLitterMate(type: "database", return_all: true)
    if dbs
      dbs.each_pair { |sib_name, sib|
        @groomer.groomer_class.grantSecretAccess(@mu_name, sib_name, "database_credentials")
        if sib.config and sib.config['auth_vault']
          @groomer.groomer_class.grantSecretAccess(@mu_name, sib.config['auth_vault']['vault'], sib.config['auth_vault']['item'])
        end
      }
    end

    if @config['groom'].nil? or @config['groom']
      @groomer.run(purpose: "Full Initial Run", max_retries: 15, reboot_first_fail: (windows? and @config['groomer'] != "Ansible"), timeout: @config['groomer_timeout'])
    end
  rescue MU::Groomer::RunError => e
    raise e if !@config['create_image'].nil? and !@config['image_created']
    MU.log "Proceeding after failed initial Groomer run, but #{@mu_name} may not behave as expected!", MU::WARN, details: e.message
  rescue StandardError => e
    raise e if !@config['create_image'].nil? and !@config['image_created']
    MU.log "Caught #{e.inspect} on #{@mu_name} in an unexpected place (after @groomer.run on Full Initial Run)", MU::ERR
  end

  if !@config['create_image'].nil? and !@config['image_created']
    createImage
  end

  MU::MommaCat.unlock(@cloud_id+"-groom")
end
listIPs() click to toggle source

Return all of the IP addresses, public and private, from all of our network interfaces. @return [Array<String>]

# File modules/mu/providers/aws/server.rb, line 2019
def listIPs
  MU::Cloud::AWS::Server.getAddresses(cloud_desc).first
end
notify() click to toggle source

Return a description of this resource appropriate for deployment metadata. Arguments reflect the return values of the MU::Cloud::.describe method

# File modules/mu/providers/aws/server.rb, line 785
def notify
  if cloud_desc.nil?
    raise MuError, "Failed to load instance metadata for #{@mu_name}/#{@cloud_id}"
  end

  interfaces = []
  private_ips = []

  cloud_desc.network_interfaces.each { |iface|
    iface.private_ip_addresses.each { |priv_ip|
      private_ips << priv_ip.private_ip_address
    }
    interfaces << {
        "network_interface_id" => iface.network_interface_id,
        "subnet_id" => iface.subnet_id,
        "vpc_id" => iface.vpc_id
    }
  }

  deploydata = {
    "nodename" => @mu_name,
    "run_list" => @config['run_list'],
    "image_created" => @config['image_created'],
    "iam_role" => @config['iam_role'],
    "cloud_desc_id" => @cloud_id,
    "private_dns_name" => cloud_desc.private_dns_name,
    "public_dns_name" => cloud_desc.public_dns_name,
    "private_ip_address" => cloud_desc.private_ip_address,
    "public_ip_address" => cloud_desc.public_ip_address,
    "private_ip_list" => private_ips,
    "key_name" => cloud_desc.key_name,
    "subnet_id" => cloud_desc.subnet_id,
    "cloud_desc_type" => cloud_desc.instance_type #,
    #                           "network_interfaces" => interfaces,
    #                           "config" => server
  }

  if !@mu_windows_name.nil?
    deploydata["mu_windows_name"] = @mu_windows_name
  end
  if !@config['chef_data'].nil?
    deploydata.merge!(@config['chef_data'])
  end
  deploydata["region"] = @region if !@region.nil?
  if !@named
    MU::MommaCat.nameKitten(self, no_dns: true)
    @named = true
  end

  return deploydata
end
postBoot(instance_id = nil) click to toggle source

Apply tags, bootstrap our configuration management, and other administravia for a new instance.

# File modules/mu/providers/aws/server.rb, line 461
def postBoot(instance_id = nil)
  @cloud_id ||= instance_id
  _node, _config, deploydata = describe(cloud_id: @cloud_id)

  raise MuError, "Couldn't find instance #{@mu_name} (#{@cloud_id})" if !cloud_desc
  return false if !MU::MommaCat.lock(@cloud_id+"-orchestrate", true)
  return false if !MU::MommaCat.lock(@cloud_id+"-groom", true)

  getIAMProfile

  finish = Proc.new { |status|
    MU::MommaCat.unlock(@cloud_id+"-orchestrate")
    MU::MommaCat.unlock(@cloud_id+"-groom")
    return status
  }

  MU::Cloud::AWS.createStandardTags(
    @cloud_id,
    region: @region,
    credentials: @credentials,
    optional: @config['optional_tags'],
    nametag: @mu_name,
    othertags: @config['tags']
  )

  # Make double sure we don't lose a cached mu_windows_name value.
  if (windows? or !@config['active_directory'].nil?)
    @mu_windows_name ||= deploydata['mu_windows_name']
  end

  loop_if = Proc.new {
    !cloud_desc(use_cache: false) or cloud_desc.state.name != "running"
  }
  MU.retrier([Aws::EC2::Errors::ServiceError], max: 30, wait: 40, loop_if: loop_if) { |retries, _wait|
    if cloud_desc and cloud_desc.state.name == "terminated"
      logs = if !@config['basis'].nil?
        pool = @deploy.findLitterMate(type: "server_pools", name: @config["name"])
        if pool
          MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_scaling_activities(auto_scaling_group_name: pool.cloud_id).activities
        else
          nil
        end
      end
      raise MuError.new, "#{@cloud_id} appears to have been terminated mid-bootstrap!", details: logs
    end
    if retries % 3 == 0
      MU.log "Waiting for EC2 instance #{@mu_name} (#{@cloud_id}) to be ready...", MU::NOTICE
    end
  }

  allowBastionAccess

  setAlarms

  # Unless we're planning on associating a different IP later, set up a
  # DNS entry for this thing and let it sync in the background. We'll come
  # back to it later.
  if @config['static_ip'].nil? and !@named
    MU::MommaCat.nameKitten(self)
    @named = true
  end

  if !@config['src_dst_check'] and !@config["vpc"].nil?
    MU.log "Disabling source_dest_check #{@mu_name} (making it NAT-worthy)"
    MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).modify_instance_attribute(
      instance_id: @cloud_id,
      source_dest_check: { value: false }
    )
  end

  # Set console termination protection. Autoscale nodes won't set this
  # by default.
  MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).modify_instance_attribute(
    instance_id: @cloud_id,
    disable_api_termination: { value: true}
  )

  tagVolumes
  configureNetworking
  saveCredentials

  if !@config['image_then_destroy']
    notify
  end

  finish.call(false) if !bootstrapGroomer

  # Make sure we got our name written everywhere applicable
  if !@named
    MU::MommaCat.nameKitten(self)
    @named = true
  end

  finish.call(true)
end
reboot(hard = false) click to toggle source

Ask the Amazon API to restart this node

# File modules/mu/providers/aws/server.rb, line 364
def reboot(hard = false)
  return if @cloud_id.nil?

  if hard
    groupname = nil
    if !@config['basis'].nil?
      resp = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_auto_scaling_instances(
        instance_ids: [@cloud_id]
      )
      groupname = resp.auto_scaling_instances.first.auto_scaling_group_name
      MU.log "Pausing Autoscale processes in #{groupname}", MU::NOTICE
      MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).suspend_processes(
        auto_scaling_group_name: groupname,
        scaling_processes: [
          "Terminate",
        ], 
      )
    end
    begin
      MU.log "Stopping #{@mu_name} (#{@cloud_id})", MU::NOTICE
      MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).stop_instances(
        instance_ids: [@cloud_id]
      )
      MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).wait_until(:instance_stopped, instance_ids: [@cloud_id]) do |waiter|
        waiter.before_attempt do
          MU.log "Waiting for #{@mu_name} to stop for hard reboot"
        end
      end
      MU.log "Starting #{@mu_name} (#{@cloud_id})"
      MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).start_instances(
        instance_ids: [@cloud_id]
      )
    ensure
      if !groupname.nil?
        MU.log "Resuming Autoscale processes in #{groupname}", MU::NOTICE
        MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).resume_processes(
          auto_scaling_group_name: groupname,
          scaling_processes: [
            "Terminate",
          ],
        )
      end
    end
  else
    MU.log "Rebooting #{@mu_name} (#{@cloud_id})"
    MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).reboot_instances(
      instance_ids: [@cloud_id]
    )
  end
end
toKitten(**_args) click to toggle source

Reverse-map our cloud description into a runnable config hash. We assume that any values we have in +@config+ are placeholders, and calculate our own accordingly based on what's live in the cloud.

# File modules/mu/providers/aws/server.rb, line 630
      def toKitten(**_args)
        bok = {
          "cloud" => "AWS",
          "credentials" => @credentials,
          "cloud_id" => @cloud_id,
          "region" => @region
        }

        if !cloud_desc
          MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config
          return nil
        end

        asgs = MU::Cloud.resourceClass("AWS", "ServerPool").find(
          instance_id: @cloud_id,
          region: @region,
          credentials: @credentials
        )
        if asgs.size > 0
          MU.log "#{@mu_name} is an Autoscale node, will be adopted under server_pools", MU::DEBUG, details: asgs
          return nil
        end

        bok['name'] = @cloud_id
        if cloud_desc.tags and !cloud_desc.tags.empty?
          bok['tags'] = MU.structToHash(cloud_desc.tags, stringify_keys: true)
          realname = MU::Adoption.tagsToName(bok['tags'])
          if realname
            bok['name'] = realname
            bok['name'].gsub!(/[^a-zA-Z0-9_\-]/, "_")
          end
        end

        bok['size'] = cloud_desc.instance_type

        if cloud_desc.vpc_id
          bok['vpc'] = MU::Config::Ref.get(
            id: cloud_desc.vpc_id,
            cloud: "AWS",
            credentials: @credentials,
            type: "vpcs",
          )
        end

        if !cloud_desc.source_dest_check
          bok['src_dst_check'] = false
        end

        bok['image_id'] = cloud_desc.image_id

        ami = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_images(image_ids: [bok['image_id']]).images.first

        if ami.nil? or ami.empty?
          MU.log "#{@mu_name} source image #{bok['image_id']} no longer exists", MU::WARN
          bok.delete("image_id")
        end

        if cloud_desc.block_device_mappings and !cloud_desc.block_device_mappings.empty?
          vol_map = {}
          MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_volumes(
            volume_ids: cloud_desc.block_device_mappings.map { |d| d.ebs.volume_id if d.ebs }
          ).volumes.each { |vol|
            vol_map[vol.volume_id] = vol
          }
          cloud_desc.block_device_mappings.each { |disk|
            if ami and ami.block_device_mappings
              is_ami_disk = false
              ami.block_device_mappings.each { |ami_dev|
                is_ami_disk = true if ami_dev.device_name == disk.device_name
              }
              next if is_ami_disk
            end
            disk_desc = { "device" => disk.device_name }
            if disk.ebs and disk.ebs.volume_id and vol_map[disk.ebs.volume_id]
              disk_desc["size"] = vol_map[disk.ebs.volume_id].size
              disk_desc["delete_on_termination"] = disk.ebs.delete_on_termination
              if vol_map[disk.ebs.volume_id].encrypted
                disk_desc['encrypted'] = true
              end
              if vol_map[disk.ebs.volume_id].iops
                disk_desc['iops'] = vol_map[disk.ebs.volume_id].iops
              end
              disk_desc["volume_type"] = vol_map[disk.ebs.volume_id].volume_type
            end
            bok['storage'] ||= []
            bok['storage'] << disk_desc
          }
        end

        cloud_desc.network_interfaces.each { |int|
          if !bok['vpc'] and int.vpc_id
            bok['vpc'] = MU::Config::Ref.get(
              id: int.vpc_id,
              cloud: "AWS",
              credentials: @credentials,
              region: @region,
              subnet_id: int.subnet_id,
              habitat: MU::Config::Ref.get(
                id: int.owner_id,
                cloud: "AWS",
                credentials: @credentials
              )
            )
          end

          int.private_ip_addresses.each { |priv_ip|
            if !priv_ip.primary
              bok['add_private_ips'] ||= 0
              bok['add_private_ips'] += 1
            end
            if priv_ip.association and priv_ip.association.public_ip 
              bok['associate_public_ip'] = true
              if priv_ip.association.ip_owner_id != "amazon"
                bok['static_ip'] = {
                  "assign_ip" => true,
                  "ip" => priv_ip.association.public_ip
                }
              end
            end
          }

          if int.groups.size > 0

            require 'mu/providers/aws/firewall_rule'
            ifaces = MU::Cloud.resourceClass("AWS", "FirewallRule").getAssociatedInterfaces(int.groups.map { |sg| sg.group_id }, credentials: @credentials, region: @region)
            done_local_rules = false
            int.groups.each { |sg|
              if !done_local_rules and ifaces[sg.group_id].size == 1
                sg_desc = MU::Cloud.resourceClass("AWS", "FirewallRule").find(cloud_id: sg.group_id, credentials: @credentials, region: @region).values.first
                if sg_desc
                  bok["ingress_rules"] = MU::Cloud.resourceClass("AWS", "FirewallRule").rulesToBoK(sg_desc.ip_permissions)
                  bok["ingress_rules"].concat(MU::Cloud.resourceClass("AWS", "FirewallRule").rulesToBoK(sg_desc.ip_permissions_egress, egress: true))
                  done_local_rules = true
                  next
                end
              end
              bok['add_firewall_rules'] ||= []
              bok['add_firewall_rules'] << MU::Config::Ref.get(
                id: sg.group_id,
                cloud: "AWS",
                credentials: @credentials,
                type: "firewall_rules",
                region: @region
              )
            }
          end
        }

# XXX go get the got-damned instance profile

        bok
      end

Private Instance Methods

bootstrapGroomer() click to toggle source
# File modules/mu/providers/aws/server.rb, line 2025
def bootstrapGroomer
  if (@config['groom'].nil? or @config['groom']) and !@groomer.haveBootstrapped?
    MU.retrier([BootstrapTempFail], wait: 45) {
      if windows? 
        # kick off certificate generation early; WinRM will need it
        @deploy.nodeSSLCerts(self)
        @deploy.nodeSSLCerts(self, true) if @config.has_key?("basis")
        session = getWinRMSession(50, 60, reboot_on_problems: true)
        initialWinRMTasks(session)
        begin
          session.close
        rescue StandardError
          # session.close is allowed to fail- we're probably rebooting
        end
      else
        session = getSSHSession(40, 30)
        initialSSHTasks(session)
      end
    }
  end

  # See if this node already exists in our config management. If it
  # does, we're done.

  if MU.inGem?
    MU.log "Deploying from a gem, not grooming"
  elsif @config['groom'].nil? or @config['groom']
    if @groomer.haveBootstrapped?
      MU.log "Node #{@mu_name} has already been bootstrapped, skipping groomer setup.", MU::NOTICE
    else
      begin
        @groomer.bootstrap
      rescue MU::Groomer::RunError
        return false
      end
    end
    @groomer.saveDeployData
  end

  true
end
configureNetworking() click to toggle source
# File modules/mu/providers/aws/server.rb, line 2135
def configureNetworking
  if !@config['static_ip'].nil?
    if !@config['static_ip']['ip'].nil?
      MU::Cloud::AWS::Server.associateElasticIp(@cloud_id, classic: @vpc.nil?, ip: @config['static_ip']['ip'], credentials: @credentials)
    elsif !haveElasticIP?
      MU::Cloud::AWS::Server.associateElasticIp(@cloud_id, classic: @vpc.nil?, credentials: @credentials)
    end
  end

  if !@vpc.nil? and @config.has_key?("vpc")
    subnet = @vpc.getSubnet(cloud_id: cloud_desc.subnet_id)

    _nat_ssh_key, _nat_ssh_user, nat_ssh_host, _canonical_ip, _ssh_user, _ssh_key_name = getSSHConfig
    if subnet.private? and !nat_ssh_host and !MU::Cloud.resourceClass("AWS", "VPC").haveRouteToInstance?(cloud_desc, region: @region, credentials: @credentials)
      raise MuError, "#{@mu_name} is in a private subnet (#{subnet}), but has no bastion host configured, and I have no other route to it"
    end

    # If we've asked for additional subnets (and this @config is not a
    # member of a Server Pool, which has different semantics), create
    # extra interfaces to accomodate.
    if !@config['vpc']['subnets'].nil? and @config['basis'].nil?
      device_index = 1
      mySubnets.each { |s|
        next if s.cloud_id == cloud_desc.subnet_id

        if cloud_desc.placement.availability_zone != s.az
          MU.log "Cannot create interface in subnet #{s.to_s} for #{@mu_name} due to AZ mismatch", MU::WARN
          next
        end
        MU.log "Adding network interface on subnet #{s.cloud_id} for #{@mu_name}"
        iface = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).create_network_interface(subnet_id: s.cloud_id).network_interface
        MU::Cloud::AWS.createStandardTags(
          iface.network_interface_id,
          region: @region,
          credentials: @credentials,
          optional: @config['optional_tags'],
          nametag: @mu_name+"-ETH"+device_index.to_s,
          othertags: @config['tags']
        )

        MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).attach_network_interface(
          network_interface_id: iface.network_interface_id,
          instance_id: cloud_desc.instance_id,
          device_index: device_index
        )
        device_index = device_index + 1
      }
      cloud_desc(use_cache: false)
    end
  end

  [:private_dns_name, :public_dns_name, :private_ip_address, :public_ip_address].each { |field|
    @config[field.to_s] = cloud_desc.send(field)
  }

  if !@config['add_private_ips'].nil?
    cloud_desc.network_interfaces.each { |int|
      if int.private_ip_address == cloud_desc.private_ip_address and int.private_ip_addresses.size < (@config['add_private_ips'] + 1)
        MU.log "Adding #{@config['add_private_ips']} extra private IP addresses to #{cloud_desc.instance_id}"
        MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).assign_private_ip_addresses(
          network_interface_id: int.network_interface_id,
          secondary_private_ip_address_count: @config['add_private_ips'],
          allow_reassignment: false
        )
      end
    }
  end
end
createImage() click to toggle source
# File modules/mu/providers/aws/server.rb, line 2375
def createImage
  img_cfg = @config['create_image']
  # Scrub things that don't belong on an AMI
  session = windows? ? getWinRMSession : getSSHSession
  sudo = purgecmd = ""
  sudo = "sudo" if @config['ssh_user'] != "root"
  if windows?
    purgecmd = "rm -rf /cygdrive/c/mu_installed_chef"
  else
    purgecmd = "rm -rf /opt/mu_installed_chef"
  end
  if img_cfg['image_then_destroy']
    if windows?
      purgecmd = "rm -rf /cygdrive/c/chef/ /home/#{@config['windows_admin_username']}/.ssh/authorized_keys /home/Administrator/.ssh/authorized_keys /cygdrive/c/mu-installer-ran-updates /cygdrive/c/mu_installed_chef"
      # session.exec!("powershell -Command \"& {(Get-WmiObject -Class Win32_Product -Filter \"Name='UniversalForwarder'\").Uninstall()}\"")
    else
      purgecmd = "#{sudo} rm -rf /var/lib/cloud/instances/i-* /root/.ssh/authorized_keys /etc/ssh/ssh_host_*key* /etc/chef /etc/opscode/* /.mu-installer-ran-updates /var/chef /opt/mu_installed_chef /opt/chef ; #{sudo} sed -i 's/^HOSTNAME=.*//' /etc/sysconfig/network"
    end
  end
  if windows?
    session.run(purgecmd)
  else
    session.exec!(purgecmd)
  end
  session.close
  ami_ids = MU::Cloud::AWS::Server.createImage(
      name: @mu_name,
      instance_id: @cloud_id,
      storage: @config['storage'],
      exclude_storage: img_cfg['image_exclude_storage'],
      copy_to_regions: img_cfg['copy_to_regions'],
      make_public: img_cfg['public'],
      region: @region,
      tags: @config['tags'],
      credentials: @credentials
  )

  @deploy.notify("images", @config['name'], ami_ids)
  @config['image_created'] = true
  if img_cfg['image_then_destroy']
    MU::Cloud::AWS::Server.waitForAMI(ami_ids[@region], region: @region, credentials: @credentials)
    MU.log "AMI #{ami_ids[@region]} ready, removing source node #{@mu_name}"
    MU::Cloud::AWS::Server.terminateInstance(id: @cloud_id, region: @region, deploy_id: @deploy.deploy_id, mu_name: @mu_name, credentials: @credentials)
    destroy
  end
end
getIAMProfile() click to toggle source
# File modules/mu/providers/aws/server.rb, line 2271
def getIAMProfile
  self.class.getIAMProfile(
    @config['name'],
    @deploy,
    generated: @config['generate_iam_role'],
    role_name: @config['iam_role'],
    region: @region,
    credentials: @credentials,
    want_arn: true
  )
end
haveElasticIP?() click to toggle source
# File modules/mu/providers/aws/server.rb, line 2120
def haveElasticIP?
  if !cloud_desc.public_ip_address.nil?
    begin
      resp = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_addresses(public_ips: [cloud_desc.public_ip_address])
      if resp.addresses.size > 0 and resp.addresses.first.instance_id == @cloud_id
        return true
      end
    rescue Aws::EC2::Errors::InvalidAddressNotFound
      # XXX this is ok to ignore, it means the public IP isn't Elastic
    end
  end

  false
end
saveCredentials(win_admin_password = nil) click to toggle source
# File modules/mu/providers/aws/server.rb, line 2067
def saveCredentials(win_admin_password = nil)
  ec2config_password = nil
  sshd_password = nil
  if windows?
    if @config['use_cloud_provider_windows_password']
      win_admin_password ||= getWindowsAdminPassword
    elsif @config['windows_auth_vault'] and !@config['windows_auth_vault'].empty?
      if @config["windows_auth_vault"].has_key?("password_field")
        win_admin_password ||= @groomer.getSecret(
          vault: @config['windows_auth_vault']['vault'],
          item: @config['windows_auth_vault']['item'],
          field: @config["windows_auth_vault"]["password_field"]
        )
      else
        win_admin_password ||= getWindowsAdminPassword
      end

      if @config["windows_auth_vault"].has_key?("ec2config_password_field")
        ec2config_password = @groomer.getSecret(
          vault: @config['windows_auth_vault']['vault'],
          item: @config['windows_auth_vault']['item'],
          field: @config["windows_auth_vault"]["ec2config_password_field"]
        )
      end

      if @config["windows_auth_vault"].has_key?("sshd_password_field")
        sshd_password = @groomer.getSecret(
          vault: @config['windows_auth_vault']['vault'],
          item: @config['windows_auth_vault']['item'],
          field: @config["windows_auth_vault"]["sshd_password_field"]
        )
      end
    end

    win_admin_password ||= MU.generateWindowsPassword
    ec2config_password ||= MU.generateWindowsPassword
    sshd_password ||= MU.generateWindowsPassword

    # We're creating the vault here so when we run
    # MU::Cloud::Server.initialSSHTasks and we need to set the Windows
    # Admin password we can grab it from said vault.
    creds = {
      "username" => @config['windows_admin_username'],
      "password" => win_admin_password,
      "ec2config_username" => "ec2config",
      "ec2config_password" => ec2config_password,
      "sshd_username" => "sshd_service",
      "sshd_password" => sshd_password
    }
    @groomer.saveSecret(vault: @mu_name, item: "windows_credentials", data: creds, permissions: "name:#{@mu_name}")
  end
end
setAlarms() click to toggle source

If we came up via AutoScale, the Alarm module won't have had our instance ID to associate us with itself. So invoke that here. XXX might be possible to do this with regular alarm resources and dependencies now

# File modules/mu/providers/aws/server.rb, line 2227
def setAlarms
  if !@config['basis'].nil? and @config["alarms"] and !@config["alarms"].empty?
    @config["alarms"].each { |alarm|
      alarm_obj = MU::MommaCat.findStray(
        "AWS",
        "alarms",
        region: @region,
        deploy_id: @deploy.deploy_id,
        name: alarm['name']
      ).first
      alarm["dimensions"] = [{:name => "InstanceId", :value => @cloud_id}]

      if alarm["enable_notifications"]
        # XXX vile, this should be a sibling resource generated by the
        # parser
        topic_arn = MU::Cloud.resourceClass("AWS", "Notification").createTopic(alarm["notification_group"], region: @region, credentials: @credentials)
        MU::Cloud.resourceClass("AWS", "Notification").subscribe(topic_arn, alarm["notification_endpoint"], alarm["notification_type"], region: @region, credentials: @credentials)
        alarm["alarm_actions"] = [topic_arn]
        alarm["ok_actions"]  = [topic_arn]
      end

      alarm_name = alarm_obj ? alarm_obj.cloud_id : "#{@mu_name}-#{alarm['name']}".upcase

      MU::Cloud.resourceClass("AWS", "Alarm").setAlarm(
        name: alarm_name,
        ok_actions: alarm["ok_actions"],
        alarm_actions: alarm["alarm_actions"],
        insufficient_data_actions: alarm["no_data_actions"],
        metric_name: alarm["metric_name"],
        namespace: alarm["namespace"],
        statistic: alarm["statistic"],
        dimensions: alarm["dimensions"],
        period: alarm["period"],
        unit: alarm["unit"],
        evaluation_periods: alarm["evaluation_periods"],
        threshold: alarm["threshold"],
        comparison_operator: alarm["comparison_operator"],
        region: @region,
        credentials: @credentials
      )
    }
  end
end
setDeleteOntermination(device, delete_on_termination = false) click to toggle source
# File modules/mu/providers/aws/server.rb, line 2352
def setDeleteOntermination(device, delete_on_termination = false)
  mappings = MU.structToHash(cloud_desc.block_device_mappings)
  mappings.each { |vol|
    if vol[:ebs]
      vol[:ebs].delete(:attach_time)
      vol[:ebs].delete(:status)
    end
    if vol[:device_name] == device
      if vol[:ebs][:delete_on_termination] != delete_on_termination
        vol[:ebs][:delete_on_termination] = delete_on_termination
        MU.log "Setting delete_on_termination flag to #{delete_on_termination.to_s} on #{@mu_name}'s #{device}"
        MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).modify_instance_attribute(
          instance_id: @cloud_id,
          block_device_mappings: mappings
        )
      end
      return true
    end
  }

  false
end
tagVolumes() click to toggle source
# File modules/mu/providers/aws/server.rb, line 2204
def tagVolumes
  volumes = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_volumes(filters: [name: "attachment.instance-id", values: [@cloud_id]])
  volumes.each { |vol|
    vol.volumes.each { |volume|
      volume.attachments.each { |attachment|
        MU::Cloud::AWS.createStandardTags(
          attachment.volume_id,
          region: @region,
          credentials: @credentials,
          optional: @config['optional_tags'],
          nametag: ["/dev/sda", "/dev/sda1"].include?(attachment.device) ? "ROOT-"+@mu_name : @mu_name+"-"+attachment.device.upcase,
          othertags: @config['tags']
        )
  
      }
    }
  }
end