modules/mu/clouds/aws/server.rb in cloud-mu-3.0.2 vs modules/mu/clouds/aws/server.rb in cloud-mu-3.1.0

- old
+ new

@@ -94,993 +94,1177 @@ "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 + "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 + @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 + 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 - 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 - + @mu_name = @deploy.getResourceName(@config['name']) end + @config['mu_name'] = @mu_name - @config['instance_secret'] ||= Password.random(50) - - @groomer = MU::Groomer.new(self) unless MU.inGem? end - @@userdata_semaphore = Mutex.new + @config['instance_secret'] ||= Password.random(50) - # 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] - 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/clouds/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 + @groomer = MU::Groomer.new(self) unless MU.inGem? + end + + @@userdata_semaphore = Mutex.new + + # 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] + 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/clouds/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.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 = erbfile + script = +erb.result else - script = script+"\n"+erbfile + script = script+"\n"+erb.result end - MU.log "Parsed #{custom_append['path']} as flat file", MU::DEBUG, details: script + 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 - return script - } - end + end + return script + } + end - # 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] - 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::MommaCat.createTag(vol_id, tag_name, tag_value, region: region, credentials: credentials) - break - end - } + # 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] + 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::MommaCat.createTag(vol_id, tag_name, tag_value, region: region, credentials: credentials) + break + end } } - end + } + end - # Called automatically by {MU::Deploy#createResources} - def create - begin - done = false - instance = createEc2Instance + # Called automatically by {MU::Deploy#createResources} + 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") + @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") + 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::Cloud::AWS.createStandardTags(instance.instance_id, region: @config['region'], credentials: @config['credentials']) - MU::MommaCat.createTag(instance.instance_id, "Name", @mu_name, region: @config['region'], credentials: @config['credentials']) + MU.log "Node creation complete for #{@config['name']}" end - done = true - rescue Exception => 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: @config['region'], credentials: @config['credentials'], flags: { "skipsnapshots" => true } ) - } - end + MU::MommaCat.unlock(instance.instance_id+"-create") + else + MU::Cloud::AWS.createStandardTags(instance.instance_id, region: @config['region'], credentials: @config['credentials']) + MU::MommaCat.createTag(instance.instance_id, "Name", @mu_name, region: @config['region'], credentials: @config['credentials']) + end + done = true + rescue Exception => 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: @config['region'], credentials: @config['credentials'], flags: { "skipsnapshots" => true } ) + } end - raise e end - - return @config + raise e end + return @config + end - # Create an Amazon EC2 instance. - def createEc2Instance - name = @config["name"] - node = @config['mu_name'] - instance_descriptor = { - :image_id => @config["ami_id"], - :key_name => @deploy.ssh_key_name, - :instance_type => @config["size"], - :disable_api_termination => true, - :min_count => 1, - :max_count => 1 - } + # Create an Amazon EC2 instance. + def createEc2Instance + name = @config["name"] + node = @config['mu_name'] - arn = nil - if @config['generate_iam_role'] - role = @deploy.findLitterMate(name: @config['name'], type: "roles") - 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?(@config['region']) ? "aws-us-gov" : "aws")+':s3:::'+MU::Cloud::AWS.adminBucketName(@credentials)+'/'+file - } - MU.log "Adding S3 read permissions to #{@mu_name}'s IAM profile", MU::NOTICE, details: s3_objs - role.cloudobj.injectPolicyTargets("MuSecrets", s3_objs) + instance_descriptor = { + :image_id => @config["ami_id"], + :key_name => @deploy.ssh_key_name, + :instance_type => @config["size"], + :disable_api_termination => true, + :min_count => 1, + :max_count => 1 + } - @config['iam_role'] = role.mu_name - arn = role.cloudobj.createInstanceProfile + arn = nil + if @config['generate_iam_role'] + role = @deploy.findLitterMate(name: @config['name'], type: "roles") + 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?(@config['region']) ? "aws-us-gov" : "aws")+':s3:::'+MU::Cloud::AWS.adminBucketName(@credentials)+'/'+file + } + MU.log "Adding S3 read permissions to #{@mu_name}'s IAM profile", MU::NOTICE, details: s3_objs + role.cloudobj.injectPolicyTargets("MuSecrets", s3_objs) + + @config['iam_role'] = role.mu_name + arn = role.cloudobj.createInstanceProfile # @cfm_role_name, @cfm_prof_name - elsif @config['iam_role'].nil? - raise MuError, "#{@mu_name} has generate_iam_role set to false, but no iam_role assigned." + elsif @config['iam_role'].nil? + raise MuError, "#{@mu_name} has generate_iam_role set to false, but no iam_role assigned." + end + if !@config["iam_role"].nil? + if arn + instance_descriptor[:iam_instance_profile] = {arn: arn} + else + instance_descriptor[:iam_instance_profile] = {name: @config["iam_role"]} end - if !@config["iam_role"].nil? - if arn - instance_descriptor[:iam_instance_profile] = {arn: arn} - else - instance_descriptor[:iam_instance_profile] = {name: @config["iam_role"]} - end - end + end - security_groups = [] - if @dependencies.has_key?("firewall_rule") - @dependencies['firewall_rule'].values.each { |sg| - security_groups << sg.cloud_id - } - end + security_groups = [] + if @dependencies.has_key?("firewall_rule") + @dependencies['firewall_rule'].values.each { |sg| + security_groups << sg.cloud_id + } + end - 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 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'].nil? - instance_descriptor[:private_ip_address] = @config['private_ip'] - end + if !@config['private_ip'].nil? + instance_descriptor[:private_ip_address] = @config['private_ip'] + end - vpc_id = subnet = nil - if !@vpc.nil? and @config.has_key?("vpc") - subnet_conf = @config['vpc'] - subnet_conf = @config['vpc']['subnets'].first if @config['vpc'].has_key?("subnets") and !@config['vpc']['subnets'].empty? - tag_key, tag_value = subnet_conf['tag'].split(/=/, 2) if !subnet_conf['tag'].nil? + vpc_id = subnet = nil + if !@vpc.nil? and @config.has_key?("vpc") + subnet_conf = @config['vpc'] + subnet_conf = @config['vpc']['subnets'].first if @config['vpc'].has_key?("subnets") and !@config['vpc']['subnets'].empty? + tag_key, tag_value = subnet_conf['tag'].split(/=/, 2) if !subnet_conf['tag'].nil? - subnet = @vpc.getSubnet( - cloud_id: subnet_conf['subnet_id'], - name: subnet_conf['subnet_name'], - tag_key: tag_key, - tag_value: tag_value - ) - if subnet.nil? - raise MuError, "Got null subnet id out of #{subnet_conf['vpc']}" - end - MU.log "Deploying #{node} into VPC #{@vpc.cloud_id} Subnet #{subnet.cloud_id}" - punchAdminNAT - instance_descriptor[:subnet_id] = subnet.cloud_id + subnet = @vpc.getSubnet( + cloud_id: subnet_conf['subnet_id'], + name: subnet_conf['subnet_name'], + tag_key: tag_key, + tag_value: tag_value + ) + if subnet.nil? + raise MuError, "Got null subnet id out of #{subnet_conf['vpc']}" end + MU.log "Deploying #{node} into VPC #{@vpc.cloud_id} Subnet #{subnet.cloud_id}" + punchAdminNAT + instance_descriptor[:subnet_id] = subnet.cloud_id + end - if !@userdata.nil? and !@userdata.empty? - instance_descriptor[:user_data] = Base64.encode64(@userdata) - end + if !@userdata.nil? and !@userdata.empty? + instance_descriptor[:user_data] = Base64.encode64(@userdata) + end - MU::Cloud::AWS::Server.waitForAMI(@config["ami_id"], region: @config['region'], credentials: @config['credentials']) + MU::Cloud::AWS::Server.waitForAMI(@config["ami_id"], region: @config['region'], credentials: @config['credentials']) - # Figure out which devices are embedded in the AMI already. - image = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_images(image_ids: [@config["ami_id"]]).images.first - ext_disks = {} - 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 + # Figure out which devices are embedded in the AMI already. + image = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_images(image_ids: [@config["ami_id"]]).images.first + ext_disks = {} + 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 - configured_storage = Array.new - cfm_volume_map = {} - if @config["storage"] - @config["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 + configured_storage = Array.new + cfm_volume_map = {} + if @config["storage"] + @config["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 - mapping, cfm_mapping = MU::Cloud::AWS::Server.convertBlockDeviceMapping(vol) - configured_storage << mapping - } - end + end + mapping, cfm_mapping = MU::Cloud::AWS::Server.convertBlockDeviceMapping(vol) + configured_storage << mapping + } + end - instance_descriptor[:block_device_mappings] = configured_storage - instance_descriptor[:block_device_mappings].concat(@ephemeral_mappings) - instance_descriptor[:monitoring] = {enabled: @config['monitoring']} + instance_descriptor[:block_device_mappings] = configured_storage + instance_descriptor[:block_device_mappings].concat(@ephemeral_mappings) + 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 + 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 #{node}" - MU.log "Instance details for #{node}: #{instance_descriptor}", MU::DEBUG + MU.log "Creating EC2 instance #{node}" + MU.log "Instance details for #{node}: #{instance_descriptor}", MU::DEBUG # if instance_descriptor[:block_device_mappings].empty? # instance_descriptor.delete(:block_device_mappings) # end - retries = 0 - instance = begin - response = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).run_instances(instance_descriptor) - if response and response.instances and response.instances.size > 0 - instance = response.instances.first - else - MU.log "halp", MU::ERR, details: response + retries = 0 + instance = begin + response = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).run_instances(instance_descriptor) + if response and response.instances and response.instances.size > 0 + instance = response.instances.first + else + MU.log "halp", MU::ERR, details: response + end + rescue Aws::EC2::Errors::InvalidRequest => e + MU.log e.message, MU::ERR, details: instance_descriptor + raise e + rescue Aws::EC2::Errors::InvalidGroupNotFound, Aws::EC2::Errors::InvalidSubnetIDNotFound, Aws::EC2::Errors::InvalidParameterValue => e + if retries < 10 + if retries > 7 + MU.log "Seeing #{e.inspect} while trying to launch #{node}, retrying a few more times...", MU::WARN, details: instance_descriptor end - rescue Aws::EC2::Errors::InvalidRequest => e - MU.log e.message, MU::ERR, details: instance_descriptor - raise e - rescue Aws::EC2::Errors::InvalidGroupNotFound, Aws::EC2::Errors::InvalidSubnetIDNotFound, Aws::EC2::Errors::InvalidParameterValue => e - if retries < 10 - if retries > 7 - MU.log "Seeing #{e.inspect} while trying to launch #{node}, retrying a few more times...", MU::WARN, details: instance_descriptor - end - sleep 10 - retries = retries + 1 - retry - else - raise MuError, e.inspect - end + sleep 10 + retries = retries + 1 + retry + else + raise MuError, e.inspect end + end - MU.log "#{node} (#{instance.instance_id}) coming online" + MU.log "#{node} (#{instance.instance_id}) coming online" - instance - end + instance + end - # Ask the Amazon API to restart this node - def reboot(hard = false) - return if @cloud_id.nil? + # Ask the Amazon API to restart this node + def reboot(hard = false) + return if @cloud_id.nil? - if hard - groupname = nil - if !@config['basis'].nil? - resp = MU::Cloud::AWS.autoscale(region: @config['region'], credentials: @config['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: @config['region'], credentials: @config['credentials']).suspend_processes( + if hard + groupname = nil + if !@config['basis'].nil? + resp = MU::Cloud::AWS.autoscale(region: @config['region'], credentials: @config['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: @config['region'], credentials: @config['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: @config['region'], credentials: @config['credentials']).stop_instances( + instance_ids: [@cloud_id] + ) + MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).wait_until(:instance_stopped, instance_ids: [@cloud_id]) do |waiter| + waiter.before_attempt do |attempts| + 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: @config['region'], credentials: @config['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: @config['region'], credentials: @config['credentials']).resume_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: @config['region'], credentials: @config['credentials']).stop_instances( - instance_ids: [@cloud_id] - ) - MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).wait_until(:instance_stopped, instance_ids: [@cloud_id]) do |waiter| - waiter.before_attempt do |attempts| - 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: @config['region'], credentials: @config['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: @config['region'], credentials: @config['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: @config['region'], credentials: @config['credentials']).reboot_instances( - instance_ids: [@cloud_id] - ) end + else + MU.log "Rebooting #{@mu_name} (#{@cloud_id})" + MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).reboot_instances( + instance_ids: [@cloud_id] + ) end + end - # 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 - def getSSHConfig - node, config, deploydata = describe(cloud_id: @cloud_id) + # 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 + def getSSHConfig + node, config, deploydata = describe(cloud_id: @cloud_id) # XXX add some awesome alternate names from metadata and make sure they end # up in MU::MommaCat's ssh config wangling - ssh_keydir = Etc.getpwuid(Process.uid).dir+"/.ssh" - return nil if @config.nil? or @deploy.nil? + ssh_keydir = Etc.getpwuid(Process.uid).dir+"/.ssh" + 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::AWS::VPC.haveRouteToInstance?(cloud_desc, region: @config['region'], credentials: @config['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 + nat_ssh_key = nat_ssh_user = nat_ssh_host = nil + if !@config["vpc"].nil? and !MU::Cloud::AWS::VPC.haveRouteToInstance?(cloud_desc, region: @config['region'], credentials: @config['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! + 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 = @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 + 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 + 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] + return [nat_ssh_key, nat_ssh_user, nat_ssh_host, canonicalIP, @config['ssh_user'], @deploy.ssh_key_name] + end + + # Apply tags, bootstrap our configuration management, and other + # administravia for a new instance. + def postBoot(instance_id = nil) + if !instance_id.nil? + @cloud_id = instance_id end + node, config, deploydata = describe(cloud_id: @cloud_id) + instance = cloud_desc + raise MuError, "Couldn't find instance #{@mu_name} (#{@cloud_id})" if !instance + @cloud_id = instance.instance_id + return false if !MU::MommaCat.lock(instance.instance_id+"-orchestrate", true) + return false if !MU::MommaCat.lock(instance.instance_id+"-groom", true) - # Apply tags, bootstrap our configuration management, and other - # administravia for a new instance. - def postBoot(instance_id = nil) - if !instance_id.nil? - @cloud_id = instance_id - end - node, config, deploydata = describe(cloud_id: @cloud_id) - instance = cloud_desc - raise MuError, "Couldn't find instance #{@mu_name} (#{@cloud_id})" if !instance - @cloud_id = instance.instance_id - return false if !MU::MommaCat.lock(instance.instance_id+"-orchestrate", true) - return false if !MU::MommaCat.lock(instance.instance_id+"-groom", true) + MU::Cloud::AWS.createStandardTags(instance.instance_id, region: @config['region'], credentials: @config['credentials']) + MU::MommaCat.createTag(instance.instance_id, "Name", node, region: @config['region'], credentials: @config['credentials']) - MU::Cloud::AWS.createStandardTags(instance.instance_id, region: @config['region'], credentials: @config['credentials']) - MU::MommaCat.createTag(instance.instance_id, "Name", node, region: @config['region'], credentials: @config['credentials']) + if @config['optional_tags'] + MU::MommaCat.listOptionalTags.each { |key, value| + MU::MommaCat.createTag(instance.instance_id, key, value, region: @config['region'], credentials: @config['credentials']) + } + end - if @config['optional_tags'] - MU::MommaCat.listOptionalTags.each { |key, value| - MU::MommaCat.createTag(instance.instance_id, key, value, region: @config['region'], credentials: @config['credentials']) - } - end + if !@config['tags'].nil? + @config['tags'].each { |tag| + MU::MommaCat.createTag(instance.instance_id, tag['key'], tag['value'], region: @config['region'], credentials: @config['credentials']) + } + end + MU.log "Tagged #{node} (#{instance.instance_id}) with MU-ID=#{MU.deploy_id}", MU::DEBUG - if !@config['tags'].nil? - @config['tags'].each { |tag| - MU::MommaCat.createTag(instance.instance_id, tag['key'], tag['value'], region: @config['region'], credentials: @config['credentials']) - } + # 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 - MU.log "Tagged #{node} (#{instance.instance_id}) with MU-ID=#{MU.deploy_id}", MU::DEBUG + end - # 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'] + retries = -1 + max_retries = 30 + begin + if instance.nil? or instance.state.name != "running" + retries = retries + 1 + if !instance.nil? and instance.state.name == "terminated" + raise MuError, "#{@cloud_id} appears to have been terminated mid-bootstrap!" end - end - - retries = -1 - max_retries = 30 - begin - if instance.nil? or instance.state.name != "running" - retries = retries + 1 - if !instance.nil? and instance.state.name == "terminated" - raise MuError, "#{@cloud_id} appears to have been terminated mid-bootstrap!" - end - if retries % 3 == 0 - MU.log "Waiting for EC2 instance #{node} (#{@cloud_id}) to be ready...", MU::NOTICE - end - sleep 40 - # Get a fresh AWS descriptor - instance = MU::Cloud::Server.find(cloud_id: @cloud_id, region: @config['region'], credentials: @config['credentials']).values.first - if instance and instance.state.name == "terminated" - raise MuError, "EC2 instance #{node} (#{@cloud_id}) terminating during bootstrap!" - end + if retries % 3 == 0 + MU.log "Waiting for EC2 instance #{node} (#{@cloud_id}) to be ready...", MU::NOTICE end - rescue Aws::EC2::Errors::ServiceError => e - if retries < max_retries - MU.log "Got #{e.inspect} during initial instance creation of #{@cloud_id}, retrying...", MU::NOTICE, details: instance - retries = retries + 1 - retry - else - raise MuError, "Too many retries creating #{node} (#{e.inspect})" + sleep 40 + # Get a fresh AWS descriptor + instance = MU::Cloud::Server.find(cloud_id: @cloud_id, region: @config['region'], credentials: @config['credentials']).values.first + if instance and instance.state.name == "terminated" + raise MuError, "EC2 instance #{node} (#{@cloud_id}) terminating during bootstrap!" end - end while instance.nil? or (instance.state.name != "running" and retries < max_retries) + end + rescue Aws::EC2::Errors::ServiceError => e + if retries < max_retries + MU.log "Got #{e.inspect} during initial instance creation of #{@cloud_id}, retrying...", MU::NOTICE, details: instance + retries = retries + 1 + retry + else + raise MuError, "Too many retries creating #{node} (#{e.inspect})" + end + end while instance.nil? or (instance.state.name != "running" and retries < max_retries) - punchAdminNAT + punchAdminNAT - # 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 - if !@config['basis'].nil? and @config["alarms"] and !@config["alarms"].empty? - @config["alarms"].each { |alarm| - alarm_obj = MU::MommaCat.findStray( - "AWS", - "alarms", - region: @config["region"], - deploy_id: @deploy.deploy_id, - name: alarm['name'] - ).first - alarm["dimensions"] = [{:name => "InstanceId", :value => @cloud_id}] + # 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 + if !@config['basis'].nil? and @config["alarms"] and !@config["alarms"].empty? + @config["alarms"].each { |alarm| + alarm_obj = MU::MommaCat.findStray( + "AWS", + "alarms", + region: @config["region"], + deploy_id: @deploy.deploy_id, + name: alarm['name'] + ).first + alarm["dimensions"] = [{:name => "InstanceId", :value => @cloud_id}] - if alarm["enable_notifications"] - topic_arn = MU::Cloud::AWS::Notification.createTopic(alarm["notification_group"], region: @config["region"], credentials: @config['credentials']) - MU::Cloud::AWS::Notification.subscribe(arn: topic_arn, protocol: alarm["notification_type"], endpoint: alarm["notification_endpoint"], region: @config["region"], credentials: @config["credentials"]) - alarm["alarm_actions"] = [topic_arn] - alarm["ok_actions"] = [topic_arn] - end + if alarm["enable_notifications"] + topic_arn = MU::Cloud::AWS::Notification.createTopic(alarm["notification_group"], region: @config["region"], credentials: @config['credentials']) + MU::Cloud::AWS::Notification.subscribe(arn: topic_arn, protocol: alarm["notification_type"], endpoint: alarm["notification_endpoint"], region: @config["region"], credentials: @config["credentials"]) + alarm["alarm_actions"] = [topic_arn] + alarm["ok_actions"] = [topic_arn] + end - alarm_name = alarm_obj ? alarm_obj.cloud_id : "#{node}-#{alarm['name']}".upcase + alarm_name = alarm_obj ? alarm_obj.cloud_id : "#{node}-#{alarm['name']}".upcase - MU::Cloud::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: @config["region"], - credentials: @config['credentials'] - ) - } - end + MU::Cloud::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: @config["region"], + credentials: @config['credentials'] + ) + } + end - # We have issues sometimes where our dns_records are pointing at the wrong node name and IP address. - # Make sure that doesn't happen. Happens with server pools only - if @config['dns_records'] && !@config['dns_records'].empty? - @config['dns_records'].each { |dnsrec| - if dnsrec.has_key?("name") - if dnsrec['name'].start_with?(MU.deploy_id.downcase) && !dnsrec['name'].start_with?(node.downcase) - MU.log "DNS records for #{node} seem to be wrong, deleting from current config", MU::WARN, details: dnsrec - dnsrec.delete('name') - dnsrec.delete('target') - end + # We have issues sometimes where our dns_records are pointing at the wrong node name and IP address. + # Make sure that doesn't happen. Happens with server pools only + if @config['dns_records'] && !@config['dns_records'].empty? + @config['dns_records'].each { |dnsrec| + if dnsrec.has_key?("name") + if dnsrec['name'].start_with?(MU.deploy_id.downcase) && !dnsrec['name'].start_with?(node.downcase) + MU.log "DNS records for #{node} seem to be wrong, deleting from current config", MU::WARN, details: dnsrec + dnsrec.delete('name') + dnsrec.delete('target') end - } - end + end + } + end - # 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? && !@named - MU::MommaCat.nameKitten(self) - @named = true - end + # 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? && !@named + MU::MommaCat.nameKitten(self) + @named = true + end - if !@config['src_dst_check'] and !@config["vpc"].nil? - MU.log "Disabling source_dest_check #{node} (making it NAT-worthy)" - MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['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. + if !@config['src_dst_check'] and !@config["vpc"].nil? + MU.log "Disabling source_dest_check #{node} (making it NAT-worthy)" MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).modify_instance_attribute( instance_id: @cloud_id, - disable_api_termination: {:value => true} + source_dest_check: {:value => false} ) + end - has_elastic_ip = false - if !instance.public_ip_address.nil? - begin - resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_addresses(public_ips: [instance.public_ip_address]) - if resp.addresses.size > 0 and resp.addresses.first.instance_id == @cloud_id - has_elastic_ip = true - end - rescue Aws::EC2::Errors::InvalidAddressNotFound => e - # XXX this is ok to ignore, it means the public IP isn't Elastic + # Set console termination protection. Autoscale nodes won't set this + # by default. + MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).modify_instance_attribute( + instance_id: @cloud_id, + disable_api_termination: {:value => true} + ) + + has_elastic_ip = false + if !instance.public_ip_address.nil? + begin + resp = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_addresses(public_ips: [instance.public_ip_address]) + if resp.addresses.size > 0 and resp.addresses.first.instance_id == @cloud_id + has_elastic_ip = true end + rescue Aws::EC2::Errors::InvalidAddressNotFound => e + # XXX this is ok to ignore, it means the public IP isn't Elastic end + end - win_admin_password = nil - ec2config_password = nil - sshd_password = nil - if windows? - ssh_keydir = "#{Etc.getpwuid(Process.uid).dir}/.ssh" - ssh_key_name = @deploy.ssh_key_name + win_admin_password = nil + ec2config_password = nil + sshd_password = nil + if windows? + ssh_keydir = "#{Etc.getpwuid(Process.uid).dir}/.ssh" + ssh_key_name = @deploy.ssh_key_name - if @config['use_cloud_provider_windows_password'] + if @config['use_cloud_provider_windows_password'] + win_admin_password = getWindowsAdminPassword + elsif @config['windows_auth_vault'] && !@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 - elsif @config['windows_auth_vault'] && !@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 + 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?("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 + 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 if win_admin_password.nil? - ec2config_password = MU.generateWindowsPassword if ec2config_password.nil? - sshd_password = MU.generateWindowsPassword if sshd_password.nil? + win_admin_password = MU.generateWindowsPassword if win_admin_password.nil? + ec2config_password = MU.generateWindowsPassword if ec2config_password.nil? + sshd_password = MU.generateWindowsPassword if sshd_password.nil? - # 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 + # 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 - subnet = nil - if !@vpc.nil? and @config.has_key?("vpc") and !instance.subnet_id.nil? - subnet = @vpc.getSubnet( - cloud_id: instance.subnet_id - ) - if subnet.nil? - raise MuError, "Got null subnet id out of #{@config['vpc']} when asking for #{instance.subnet_id}" - end + subnet = nil + if !@vpc.nil? and @config.has_key?("vpc") and !instance.subnet_id.nil? + subnet = @vpc.getSubnet( + cloud_id: instance.subnet_id + ) + if subnet.nil? + raise MuError, "Got null subnet id out of #{@config['vpc']} when asking for #{instance.subnet_id}" end + end - if !subnet.nil? - if !subnet.private? or (!@config['static_ip'].nil? and !@config['static_ip']['assign_ip'].nil?) - if !@config['static_ip'].nil? - if !@config['static_ip']['ip'].nil? - public_ip = MU::Cloud::AWS::Server.associateElasticIp(instance.instance_id, classic: false, ip: @config['static_ip']['ip']) - elsif !has_elastic_ip - public_ip = MU::Cloud::AWS::Server.associateElasticIp(instance.instance_id) - end + if !subnet.nil? + if !subnet.private? or (!@config['static_ip'].nil? and !@config['static_ip']['assign_ip'].nil?) + if !@config['static_ip'].nil? + if !@config['static_ip']['ip'].nil? + public_ip = MU::Cloud::AWS::Server.associateElasticIp(instance.instance_id, classic: false, ip: @config['static_ip']['ip']) + elsif !has_elastic_ip + public_ip = MU::Cloud::AWS::Server.associateElasticIp(instance.instance_id) end end + end - 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::AWS::VPC.haveRouteToInstance?(cloud_desc, region: @config['region'], credentials: @config['credentials']) - raise MuError, "#{node} is in a private subnet (#{subnet}), but has no NAT host configured, and I have no other route to it" - end + 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::AWS::VPC.haveRouteToInstance?(cloud_desc, region: @config['region'], credentials: @config['credentials']) + raise MuError, "#{node} is in a private subnet (#{subnet}), but has no NAT 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 - @vpc.subnets { |s| - subnet_id = s.cloud_id - MU.log "Adding network interface on subnet #{subnet_id} for #{node}" - iface = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).create_network_interface(subnet_id: subnet_id).network_interface - MU::Cloud::AWS.createStandardTags(iface.network_interface_id, region: @config['region'], credentials: @config['credentials']) - MU::MommaCat.createTag(iface.network_interface_id, "Name", node+"-ETH"+device_index.to_s, region: @config['region'], credentials: @config['credentials']) + # 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 + @vpc.subnets { |s| + subnet_id = s.cloud_id + MU.log "Adding network interface on subnet #{subnet_id} for #{node}" + iface = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).create_network_interface(subnet_id: subnet_id).network_interface + MU::Cloud::AWS.createStandardTags(iface.network_interface_id, region: @config['region'], credentials: @config['credentials']) + MU::MommaCat.createTag(iface.network_interface_id, "Name", node+"-ETH"+device_index.to_s, region: @config['region'], credentials: @config['credentials']) - if @config['optional_tags'] - MU::MommaCat.listOptionalTags.each { |key, value| - MU::MommaCat.createTag(iface.network_interface_id, key, value, region: @config['region'], credentials: @config['credentials']) - } - end + if @config['optional_tags'] + MU::MommaCat.listOptionalTags.each { |key, value| + MU::MommaCat.createTag(iface.network_interface_id, key, value, region: @config['region'], credentials: @config['credentials']) + } + end - if !@config['tags'].nil? - @config['tags'].each { |tag| - MU::MommaCat.createTag(iface.network_interface_id, tag['key'], tag['value'], region: @config['region'], credentials: @config['credentials']) - } - end + if !@config['tags'].nil? + @config['tags'].each { |tag| + MU::MommaCat.createTag(iface.network_interface_id, tag['key'], tag['value'], region: @config['region'], credentials: @config['credentials']) + } + end - MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).attach_network_interface( - network_interface_id: iface.network_interface_id, - instance_id: instance.instance_id, - device_index: device_index - ) - device_index = device_index + 1 - } - end - elsif !@config['static_ip'].nil? - if !@config['static_ip']['ip'].nil? - public_ip = MU::Cloud::AWS::Server.associateElasticIp(instance.instance_id, classic: true, ip: @config['static_ip']['ip']) - elsif !has_elastic_ip - public_ip = MU::Cloud::AWS::Server.associateElasticIp(instance.instance_id, classic: true) - end + MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).attach_network_interface( + network_interface_id: iface.network_interface_id, + instance_id: instance.instance_id, + device_index: device_index + ) + device_index = device_index + 1 + } end - - - if !@config['image_then_destroy'] - notify + elsif !@config['static_ip'].nil? + if !@config['static_ip']['ip'].nil? + public_ip = MU::Cloud::AWS::Server.associateElasticIp(instance.instance_id, classic: true, ip: @config['static_ip']['ip']) + elsif !has_elastic_ip + public_ip = MU::Cloud::AWS::Server.associateElasticIp(instance.instance_id, classic: true) end + end - MU.log "EC2 instance #{node} has id #{instance.instance_id}", MU::DEBUG - @config["private_dns_name"] = instance.private_dns_name - @config["public_dns_name"] = instance.public_dns_name - @config["private_ip_address"] = instance.private_ip_address - @config["public_ip_address"] = instance.public_ip_address + if !@config['image_then_destroy'] + notify + end - ext_mappings = MU.structToHash(instance.block_device_mappings) + MU.log "EC2 instance #{node} has id #{instance.instance_id}", MU::DEBUG - # Root disk on standard CentOS AMI - # tagVolumes(instance.instance_id, "/dev/sda", "Name", "ROOT-"+MU.deploy_id+"-"+@config["name"].upcase) - # Root disk on standard Ubuntu AMI - # tagVolumes(instance.instance_id, "/dev/sda1", "Name", "ROOT-"+MU.deploy_id+"-"+@config["name"].upcase) + @config["private_dns_name"] = instance.private_dns_name + @config["public_dns_name"] = instance.public_dns_name + @config["private_ip_address"] = instance.private_ip_address + @config["public_ip_address"] = instance.public_ip_address - # Generic deploy ID tag - # tagVolumes(instance.instance_id) + ext_mappings = MU.structToHash(instance.block_device_mappings) - # Tag volumes with all our standard tags. - # Maybe replace tagVolumes with this? There is one more place tagVolumes is called from - volumes = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_volumes(filters: [name: "attachment.instance-id", values: [instance.instance_id]]) - volumes.each { |vol| - vol.volumes.each { |volume| - volume.attachments.each { |attachment| - MU::MommaCat.listStandardTags.each_pair { |key, value| - MU::MommaCat.createTag(attachment.volume_id, key, value, region: @config['region'], credentials: @config['credentials']) + # Root disk on standard CentOS AMI + # tagVolumes(instance.instance_id, "/dev/sda", "Name", "ROOT-"+MU.deploy_id+"-"+@config["name"].upcase) + # Root disk on standard Ubuntu AMI + # tagVolumes(instance.instance_id, "/dev/sda1", "Name", "ROOT-"+MU.deploy_id+"-"+@config["name"].upcase) - if attachment.device == "/dev/sda" or attachment.device == "/dev/sda1" - MU::MommaCat.createTag(attachment.volume_id, "Name", "ROOT-#{MU.deploy_id}-#{@config["name"].upcase}", region: @config['region'], credentials: @config['credentials']) - else - MU::MommaCat.createTag(attachment.volume_id, "Name", "#{MU.deploy_id}-#{@config["name"].upcase}-#{attachment.device.upcase}", region: @config['region'], credentials: @config['credentials']) - end - } + # Generic deploy ID tag + # tagVolumes(instance.instance_id) - if @config['optional_tags'] - MU::MommaCat.listOptionalTags.each { |key, value| - MU::MommaCat.createTag(attachment.volume_id, key, value, region: @config['region'], credentials: @config['credentials']) - } - end + # Tag volumes with all our standard tags. + # Maybe replace tagVolumes with this? There is one more place tagVolumes is called from + volumes = MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).describe_volumes(filters: [name: "attachment.instance-id", values: [instance.instance_id]]) + volumes.each { |vol| + vol.volumes.each { |volume| + volume.attachments.each { |attachment| + MU::MommaCat.listStandardTags.each_pair { |key, value| + MU::MommaCat.createTag(attachment.volume_id, key, value, region: @config['region'], credentials: @config['credentials']) - if @config['tags'] - @config['tags'].each { |tag| - MU::MommaCat.createTag(attachment.volume_id, tag['key'], tag['value'], region: @config['region'], credentials: @config['credentials']) - } + if attachment.device == "/dev/sda" or attachment.device == "/dev/sda1" + MU::MommaCat.createTag(attachment.volume_id, "Name", "ROOT-#{MU.deploy_id}-#{@config["name"].upcase}", region: @config['region'], credentials: @config['credentials']) + else + MU::MommaCat.createTag(attachment.volume_id, "Name", "#{MU.deploy_id}-#{@config["name"].upcase}-#{attachment.device.upcase}", region: @config['region'], credentials: @config['credentials']) end } - } - } - canonical_name = instance.public_dns_name - canonical_name = instance.private_dns_name if !canonical_name or nat_ssh_host != nil - @config['canonical_name'] = canonical_name + if @config['optional_tags'] + MU::MommaCat.listOptionalTags.each { |key, value| + MU::MommaCat.createTag(attachment.volume_id, key, value, region: @config['region'], credentials: @config['credentials']) + } + end - if !@config['add_private_ips'].nil? - instance.network_interfaces.each { |int| - if int.private_ip_address == instance.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 #{instance.instance_id}" - MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).assign_private_ip_addresses( - network_interface_id: int.network_interface_id, - secondary_private_ip_address_count: @config['add_private_ips'], - allow_reassignment: false - ) + if @config['tags'] + @config['tags'].each { |tag| + MU::MommaCat.createTag(attachment.volume_id, tag['key'], tag['value'], region: @config['region'], credentials: @config['credentials']) + } end } - notify - end + } + } - begin - if @config['groom'].nil? or @config['groom'] - if windows? - # kick off certificate generation early; WinRM will need it - cert, key = @deploy.nodeSSLCerts(self) - if @config.has_key?("basis") - @deploy.nodeSSLCerts(self, true) + canonical_name = instance.public_dns_name + canonical_name = instance.private_dns_name if !canonical_name or nat_ssh_host != nil + @config['canonical_name'] = canonical_name + + if !@config['add_private_ips'].nil? + instance.network_interfaces.each { |int| + if int.private_ip_address == instance.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 #{instance.instance_id}" + MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['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 + } + notify + end + + begin + if @config['groom'].nil? or @config['groom'] + if windows? + # kick off certificate generation early; WinRM will need it + cert, key = @deploy.nodeSSLCerts(self) + if @config.has_key?("basis") + @deploy.nodeSSLCerts(self, true) + end + if !@groomer.haveBootstrapped? + session = getWinRMSession(50, 60, reboot_on_problems: true) + initialWinRMTasks(session) + begin + session.close + rescue Exception + # this is allowed to fail- we're probably rebooting anyway end - if !@groomer.haveBootstrapped? - session = getWinRMSession(50, 60, reboot_on_problems: true) - initialWinRMTasks(session) - begin - session.close - rescue Exception - # this is allowed to fail- we're probably rebooting anyway - end - else # for an existing Windows node: WinRM, then SSH if it fails - begin - session = getWinRMSession(1, 60) - rescue Exception # yeah, yeah - session = getSSHSession(1, 60) - # XXX maybe loop at least once if this also fails? - end + else # for an existing Windows node: WinRM, then SSH if it fails + begin + session = getWinRMSession(1, 60) + rescue Exception # yeah, yeah + session = getSSHSession(1, 60) + # XXX maybe loop at least once if this also fails? end - else - session = getSSHSession(40, 30) - initialSSHTasks(session) end + else + session = getSSHSession(40, 30) + initialSSHTasks(session) end - rescue BootstrapTempFail - sleep 45 - retry - ensure - session.close if !session.nil? and !windows? end + rescue BootstrapTempFail + sleep 45 + retry + ensure + session.close if !session.nil? and !windows? + end - if @config["existing_deploys"] && !@config["existing_deploys"].empty? - @config["existing_deploys"].each { |ext_deploy| - if ext_deploy["cloud_id"] - found = MU::MommaCat.findStray( - @config['cloud'], - ext_deploy["cloud_type"], - cloud_id: ext_deploy["cloud_id"], - region: @config['region'], - dummy_ok: false - ).first + if @config["existing_deploys"] && !@config["existing_deploys"].empty? + @config["existing_deploys"].each { |ext_deploy| + if ext_deploy["cloud_id"] + found = MU::MommaCat.findStray( + @config['cloud'], + ext_deploy["cloud_type"], + cloud_id: ext_deploy["cloud_id"], + region: @config['region'], + dummy_ok: false + ).first - MU.log "Couldn't find existing resource #{ext_deploy["cloud_id"]}, #{ext_deploy["cloud_type"]}", MU::ERR if found.nil? - @deploy.notify(ext_deploy["cloud_type"], found.config["name"], found.deploydata, mu_name: found.mu_name, triggering_node: @mu_name) - elsif ext_deploy["mu_name"] && ext_deploy["deploy_id"] - MU.log "#{ext_deploy["mu_name"]} / #{ext_deploy["deploy_id"]}" - found = MU::MommaCat.findStray( - @config['cloud'], - ext_deploy["cloud_type"], - deploy_id: ext_deploy["deploy_id"], - mu_name: ext_deploy["mu_name"], - region: @config['region'], - dummy_ok: false - ).first + MU.log "Couldn't find existing resource #{ext_deploy["cloud_id"]}, #{ext_deploy["cloud_type"]}", MU::ERR if found.nil? + @deploy.notify(ext_deploy["cloud_type"], found.config["name"], found.deploydata, mu_name: found.mu_name, triggering_node: @mu_name) + elsif ext_deploy["mu_name"] && ext_deploy["deploy_id"] + MU.log "#{ext_deploy["mu_name"]} / #{ext_deploy["deploy_id"]}" + found = MU::MommaCat.findStray( + @config['cloud'], + ext_deploy["cloud_type"], + deploy_id: ext_deploy["deploy_id"], + mu_name: ext_deploy["mu_name"], + region: @config['region'], + dummy_ok: false + ).first - MU.log "Couldn't find existing resource #{ext_deploy["mu_name"]}/#{ext_deploy["deploy_id"]}, #{ext_deploy["cloud_type"]}", MU::ERR if found.nil? - @deploy.notify(ext_deploy["cloud_type"], found.config["name"], found.deploydata, mu_name: ext_deploy["mu_name"], triggering_node: @mu_name) - else - MU.log "Trying to find existing deploy, but either the cloud_id is not valid or no mu_name and deploy_id where provided", MU::ERR - 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" - - return true - elsif @groomer.haveBootstrapped? - MU.log "Node #{node} has already been bootstrapped, skipping groomer setup.", MU::NOTICE - - if @config['groom'].nil? or @config['groom'] - @groomer.saveDeployData + MU.log "Couldn't find existing resource #{ext_deploy["mu_name"]}/#{ext_deploy["deploy_id"]}, #{ext_deploy["cloud_type"]}", MU::ERR if found.nil? + @deploy.notify(ext_deploy["cloud_type"], found.config["name"], found.deploydata, mu_name: ext_deploy["mu_name"], triggering_node: @mu_name) + else + MU.log "Trying to find existing deploy, but either the cloud_id is not valid or no mu_name and deploy_id where provided", MU::ERR end + } + end - MU::MommaCat.unlock(instance.instance_id+"-orchestrate") - MU::MommaCat.unlock(instance.instance_id+"-groom") - return true - 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" - begin - @groomer.bootstrap if @config['groom'].nil? or @config['groom'] - rescue MU::Groomer::RunError - MU::MommaCat.unlock(instance.instance_id+"-groom") - MU::MommaCat.unlock(instance.instance_id+"-orchestrate") - return false - end + return true + elsif @groomer.haveBootstrapped? + MU.log "Node #{node} has already been bootstrapped, skipping groomer setup.", MU::NOTICE - # Make sure we got our name written everywhere applicable - if !@named - MU::MommaCat.nameKitten(self) - @named = true + if @config['groom'].nil? or @config['groom'] + @groomer.saveDeployData end - MU::MommaCat.unlock(instance.instance_id+"-groom") MU::MommaCat.unlock(instance.instance_id+"-orchestrate") + MU::MommaCat.unlock(instance.instance_id+"-groom") return true end - # postBoot + begin + @groomer.bootstrap if @config['groom'].nil? or @config['groom'] + rescue MU::Groomer::RunError + MU::MommaCat.unlock(instance.instance_id+"-groom") + MU::MommaCat.unlock(instance.instance_id+"-orchestrate") + return false + end - # 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 - def self.find(**args) - ip ||= args[:flags]['ip'] if args[:flags] and args[:flags]['ip'] + # Make sure we got our name written everywhere applicable + if !@named + MU::MommaCat.nameKitten(self) + @named = true + end - cloud_id = args[:cloud_id] - region = args[:region] - credentials = args[:credentials] - tag_key = args[:tag_key] - tag_value = args[:tag_value] + MU::MommaCat.unlock(instance.instance_id+"-groom") + MU::MommaCat.unlock(instance.instance_id+"-orchestrate") + return true + end - instance = nil - if !region.nil? - regions = [region] - else - regions = MU::Cloud::AWS.listRegions - end + # postBoot - found_instances = {} - search_semaphore = Mutex.new - search_threads = [] + # 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 + def self.find(**args) + ip ||= args[:flags]['ip'] if args[:flags] and args[:flags]['ip'] - # If we got an instance id, go get it - if !cloud_id.nil? and !cloud_id.empty? - regions.each { |r| - search_threads << Thread.new { - MU.log "Hunting for instance with cloud id '#{cloud_id}' in #{r}", MU::DEBUG - retries = 0 - begin - MU::Cloud::AWS.ec2(region: r, credentials: credentials).describe_instances( - instance_ids: [cloud_id], - filters: [ - {name: "instance-state-name", values: ["running", "pending"]} - ] - ).reservations.each { |resp| - if !resp.nil? and !resp.instances.nil? - resp.instances.each { |i| - search_semaphore.synchronize { - found_instances[i.instance_id] = i - } - } - end + instance = nil + if !args[:region].nil? + regions = [args[:region]] + else + regions = MU::Cloud::AWS.listRegions + end + + found = {} + search_semaphore = Mutex.new + search_threads = [] + + if !ip and !args[:cloud_id] and !args[:tag_value] + regions.each { |r| + search_threads << Thread.new { + MU::Cloud::AWS.ec2(region: r, credentials: args[:credentials]).describe_instances( + filters: [ + { + name: "instance-state-name", + values: ["running", "pending", "stopped"] } - rescue Aws::EC2::Errors::InvalidInstanceIDNotFound => e - if retries < 5 - retries = retries + 1 - sleep 5 - else - raise MuError, "#{e.inspect} in region #{r}" - end + ] + ).reservations.each { |resp| + if !resp.nil? and !resp.instances.nil? + resp.instances.each { |i| + search_semaphore.synchronize { + found[i.instance_id] = i + } + } end } } - done_threads = [] - begin - search_threads.each { |t| - joined = t.join(2) - done_threads << joined if !joined.nil? - } - end while found_instances.size < 1 and done_threads.size != search_threads.size - end + } - if found_instances.size > 0 - return found_instances - end + search_threads.each { |t| + t.join + } - # Ok, well, let's try looking it up by IP then - if instance.nil? and !ip.nil? - MU.log "Hunting for instance by IP '#{ip}'", MU::DEBUG - ["ip-address", "private-ip-address"].each { |filter| - response = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_instances( + return found + end + + # If we got an instance id, go get it + if args[:cloud_id] + regions.each { |r| + search_threads << Thread.new { + MU.log "Hunting for instance with cloud id '#{args[:cloud_id]}' in #{r}", MU::DEBUG + retries = 0 + begin + MU::Cloud::AWS.ec2(region: r, credentials: args[:credentials]).describe_instances( + instance_ids: [args[:cloud_id]], filters: [ - {name: filter, values: [ip]}, - {name: "instance-state-name", values: ["running", "pending"]} + { + name: "instance-state-name", + values: ["running", "pending", "stopped"] + } ] - ).reservations.first - instance = response.instances.first if !response.nil? + ).reservations.each { |resp| + if !resp.nil? and !resp.instances.nil? + resp.instances.each { |i| + search_semaphore.synchronize { + found[i.instance_id] = i + } + } + end + } + rescue Aws::EC2::Errors::InvalidInstanceIDNotFound => e + if retries < 5 + retries = retries + 1 + sleep 5 + else + raise MuError, "#{e.inspect} in region #{r}" + end + end } - end + } + 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 + end - if !instance.nil? - return {instance.instance_id => instance} if !instance.nil? - end + return found if found.size > 0 - # Fine, let's try it by tag. - if !tag_value.nil? - MU.log "Searching for instance by tag '#{tag_key}=#{tag_value}'", MU::DEBUG - MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_instances( + # Ok, well, let's try looking it up by IP then + if !ip.nil? + MU.log "Hunting for instance by IP '#{ip}'", MU::DEBUG + ["ip-address", "private-ip-address"].each { |filter| + regions.each { |r| + response = MU::Cloud::AWS.ec2(region: r, credentials: args[:credentials]).describe_instances( filters: [ - {name: "tag:#{tag_key}", values: [tag_value]}, - {name: "instance-state-name", values: ["running", "pending"]} + {name: filter, values: [ip]}, + {name: "instance-state-name", values: ["running", "pending", "stopped"]} ] + ).reservations.first + response.instances.each { |i| + found[i.instance_id] = i + } + } + } + end + + return found if found.size > 0 + + # Fine, let's try it by tag. + if args[:tag_value] + MU.log "Searching for instance by tag '#{args[:tag_key]}=#{args[:tag_value]}'", MU::DEBUG + regions.each { |r| + MU::Cloud::AWS.ec2(region: r, credentials: args[:credentials]).describe_instances( + filters: [ + {name: "tag:#{args[:tag_key]}", values: [args[:tag_value]]}, + {name: "instance-state-name", values: ["running", "pending", "stopped"]} + ] ).reservations.each { |resp| if !resp.nil? and resp.instances.size > 0 resp.instances.each { |i| - found_instances[i.instance_id] = i + found[i.instance_id] = i } end } + } + end + + return found + end + + # 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. + def toKitten(rootparent: nil, billing: nil, habitats: nil) + bok = { + "cloud" => "AWS", + "credentials" => @config['credentials'], + "cloud_id" => @cloud_id, + "region" => @config['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::AWS::ServerPool.find( + instance_id: @cloud_id, + region: @config['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 - return found_instances + 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: @config['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: @config['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: @config['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'] ||= [] + bok['add_private_ips'] << priv_ip.private_ip_address + 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/clouds/aws/firewall_rule' + ifaces = MU::Cloud::AWS::FirewallRule.getAssociatedInterfaces(int.groups.map { |sg| sg.group_id }, credentials: @credentials, region: @config['region']) + done_local_rules = false + int.groups.each { |sg| + if !done_local_rules and ifaces[sg.group_id].size == 1 + sg_desc = MU::Cloud::AWS::FirewallRule.find(cloud_id: sg.group_id, credentials: @credentials, region: @config['region']).values.first + if sg_desc + bok["ingress_rules"] = MU::Cloud::AWS::FirewallRule.rulesToBoK(sg_desc.ip_permissions) + bok["ingress_rules"].concat(MU::Cloud::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: @config['region'] + ) + } + end + } + +# XXX go get the got-damned instance profile + + bok + end # Return a description of this resource appropriate for deployment # metadata. Arguments reflect the return values of the MU::Cloud::[Resource].describe method def notify node, config, deploydata = describe(cloud_id: @cloud_id, update_cache: true)