cloudamatic/mu

View on GitHub
modules/mu/providers/aws/server_pool.rb

Summary

Maintainability
F
5 days
Test Coverage
# Copyright:: Copyright (c) 2014 eGlobalTech, Inc., all rights reserved
#
# Licensed under the BSD-3 license (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the root of the project or at
#
#     http://egt-labs.com/mu/LICENSE.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module MU
  class Cloud
    class AWS
      # A server pool as configured in {MU::Config::BasketofKittens::server_pools}
      class ServerPool < MU::Cloud::ServerPool

        # 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
        def initialize(**args)
          super
          @mu_name ||= @deploy.getResourceName(@config['name'])
        end

        # Called automatically by {MU::Deploy#createResources}
        def create
          MU.setVar("curRegion", @region) if !@region.nil?
          
          createUpdateLaunchConfig

          asg_options = buildOptionsHash

          MU.log "Creating AutoScale group #{@mu_name}", details: asg_options

          zones_to_try = @config["zones"]
          begin
            asg = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).create_auto_scaling_group(asg_options)
          rescue Aws::AutoScaling::Errors::ValidationError => e
            if zones_to_try != nil and zones_to_try.size > 0
              MU.log "#{e.message}, retrying with individual AZs", MU::WARN
              asg_options[:availability_zones] = [zones_to_try.pop]
              retry
            else
              MU.log e.message, MU::ERR, details: asg_options
              raise MuError, "#{e.message} creating AutoScale group #{@mu_name}"
            end
          end

          if zones_to_try != nil and zones_to_try.size < @config["zones"].size
            zones_to_try.each { |zone|
              begin
                MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group(
                    auto_scaling_group_name: @mu_name,
                    availability_zones: [zone]
                )
              rescue Aws::AutoScaling::Errors::ValidationError => e
                MU.log "Couldn't enable Availability Zone #{zone} for AutoScale Group #{@mu_name} (#{e.message})", MU::WARN
              end
            }

          end

          @cloud_id = @mu_name


          # Wait and see if we successfully bring up some instances
          attempts = 0
          begin
            sleep 5
            desc = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_auto_scaling_groups(auto_scaling_group_names: [@mu_name]).auto_scaling_groups.first
            MU.log "Looking for #{desc.min_size} instances in #{@mu_name}, found #{desc.instances.size}", MU::DEBUG
            attempts = attempts + 1
            if attempts > 25 and desc.instances.size == 0
              MU.log "No instances spun up after #{5*attempts} seconds, something's wrong with Autoscale group #{@mu_name}", MU::ERR, details: MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_scaling_activities(auto_scaling_group_name: @mu_name).activities
              raise MuError, "No instances spun up after #{5*attempts} seconds, something's wrong with Autoscale group #{@mu_name}"
            end
          end while desc.instances.size < desc.min_size
          MU.log "#{desc.instances.size} instances spinning up in #{@mu_name}"

          # If we're holding to bootstrap some nodes, do so, then set our min/max
          # sizes to their real values.
          if @config["wait_for_nodes"] > 0
            MU.log "Waiting for #{@config["wait_for_nodes"]} nodes to fully bootstrap before proceeding"
            parent_thread_id = Thread.current.object_id
            groomthreads = Array.new
            desc.instances.each { |member|
              begin
                groomthreads << Thread.new {
                  MU.dupGlobals(parent_thread_id)
                  MU.log "Initializing #{member.instance_id} in ServerPool #{@mu_name}"
                  MU::MommaCat.lock(member.instance_id+"-mommagroom")
                  begin
                    kitten = MU::Cloud::Server.new(mommacat: @deploy, kitten_cfg: @config, cloud_id: member.instance_id)
                  rescue RuntimeError => e
                    if e.message.match(/can't add a new key into hash during iteration/)
                      MU.log e.message+", retrying", MU::WARN
                      sleep 3
                      retry
                    else
                      raise e
                    end
                  end
                  MU::MommaCat.lock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies")
                  MU::MommaCat.unlock("#{kitten.cloudclass.name}_#{kitten.config["name"]}-dependencies")
                  if !kitten.postBoot(member.instance_id)
                    raise MU::Groomer::RunError, "Failure grooming #{member.instance_id}"
                  end
                  kitten.groom
                  MU::MommaCat.unlockAll
                }
              rescue MU::Groomer::RunError => e
                MU.log "Proceeding after failed initial Groomer run, but #{member.instance_id} may not behave as expected!", MU::WARN, details: e.inspect
              rescue StandardError => e
                if !member.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
                    Thread.new {
                      MU.dupGlobals(parent_thread_id)
                      MU::Cloud.resourceClass("AWS", "Server").terminateInstance(id: member.instance_id)
                    }
                  end
                end
                raise MuError, e.inspect
              end
            }
            groomthreads.each { |t|
              t.join
            }
            MU.log "Setting min_size to #{@config['min_size']} and max_size to #{@config['max_size']}"
            MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group(
              auto_scaling_group_name: @mu_name,
              min_size: @config['min_size'],
              max_size: @config['max_size']
            )
          end

          if @config['scale_in_protection']
            need_instances = @config['scale_in_protection'].match(/^\d+$/) ? @config['scale_in_protection'].to_i : @config['min_size']
            setScaleInProtection(need_instances)
          end

          return asg
        end

        # Make sure we have a set of instances with scale-in protection set which jives with our config
        # @param need_instances [Integer]: The number of instanceswhich must have scale-in protection set
        def setScaleInProtection(need_instances = @config['min_size'])
          live_instances = []
          begin
            desc = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_auto_scaling_groups(auto_scaling_group_names: [@mu_name]).auto_scaling_groups.first

            live_instances = desc.instances.map { |i| i.instance_id }
            already_set = 0
            desc.instances.each { |i|
              already_set += 1 if i.protected_from_scale_in
            }
            if live_instances.size < need_instances
              sleep 5
            elsif already_set > need_instances
              unset_me = live_instances.sample(already_set - need_instances)
              MU.log "Disabling scale-in protection for #{unset_me.size.to_s} instances in #{@mu_name}", MU::NOTICE, details: unset_me
              MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).set_instance_protection(
                auto_scaling_group_name: @mu_name,
                instance_ids: unset_me,
                protected_from_scale_in: false
              )
            elsif already_set < need_instances
              live_instances = live_instances.sample(need_instances)
              MU.log "Enabling scale-in protection for #{@config['scale_in_protection']} instances in #{@mu_name}", details: live_instances
              begin
                MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).set_instance_protection(
                  auto_scaling_group_name: @mu_name,
                  instance_ids: live_instances,
                  protected_from_scale_in: true
                )
              rescue Aws::AutoScaling::Errors::ValidationError => e
                if e.message.match(/not in InService/i)
                  sleep 5
                  retry
                else
                  raise e
                end
              end
            end
          end while live_instances.size < need_instances
        end

        # List out the nodes that are members of this pool
        # @return [Array<MU::Cloud::Server>]
        def listNodes
          nodes = []
          me = MU::Cloud::AWS::ServerPool.find(cloud_id: cloud_id).values.first
          if me and me.instances
            me.instances.each { |instance|
              found = MU::MommaCat.findStray("AWS", "server", cloud_id: instance.instance_id, region: @region, dummy_ok: true)
              nodes.concat(found)
            }
          end
          nodes
        end

        # Called automatically by {MU::Deploy#createResources}
        def groom
          if @config['notifications'] and @config['notifications']['topic']
# XXX expand to a full reference block for a Notification resource
            arn = if @config['notifications']['topic'].match(/^arn:/)
              @config['notifications']['topic']
            else
              "arn:#{MU::Cloud::AWS.isGovCloud?(@region) ? "aws-us-gov" : "aws"}:sns:#{@region}:#{MU::Cloud::AWS.credToAcct(@credentials)}:#{@config['notifications']['topic']}"
            end
            eventmap = {
              "launch" => "autoscaling:EC2_INSTANCE_LAUNCH",
              "failed_launch" => "autoscaling:EC2_INSTANCE_LAUNCH_ERROR",
              "terminate" => "autoscaling:EC2_INSTANCE_TERMINATE",
              "failed_terminate" => "autoscaling:EC2_INSTANCE_TERMINATE_ERROR"
            }
            MU.log "Sending simple notifications (#{@config['notifications']['events'].join(", ")}) to #{arn}"
            MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).put_notification_configuration(
              auto_scaling_group_name: @mu_name,
              topic_arn: arn,
              notification_types: @config['notifications']['events'].map { |e|
                eventmap[e]
              }
            )
          end

          if @config['schedule']
            ext_actions = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_scheduled_actions(
              auto_scaling_group_name: @mu_name
            ).scheduled_update_group_actions


            @config['schedule'].each { |s|
              sched_config = {
                :auto_scaling_group_name => @mu_name,
                :scheduled_action_name => s['action_name']
              }
              ['max_size', 'min_size', 'desired_capacity', 'recurrence'].each { |flag|
                sched_config[flag.to_sym] = s[flag] if s[flag]
              }
              ['start_time', 'end_time'].each { |flag|
                sched_config[flag.to_sym] = Time.parse(s[flag]) if s[flag]
              }
              action_already_correct = false
              ext_actions.each { |ext|
                if s['action_name'] == ext.scheduled_action_name
                  if !MU.hashCmp(MU.structToHash(ext), sched_config, missing_is_default: true)
                    MU.log "Removing scheduled action #{s['action_name']} from AutoScale group #{@mu_name}"
                    MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_scheduled_action(
                      auto_scaling_group_name: @mu_name,
                      scheduled_action_name: s['action_name']
                    )
                  else
                    action_already_correct = true
                  end
                  break
                end
              }
              if !action_already_correct
                MU.log "Adding scheduled action to AutoScale group #{@mu_name}", MU::NOTICE, details: sched_config
                MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).put_scheduled_update_group_action(
                  sched_config
                )
              end
            }
          end

          createUpdateLaunchConfig

          current = cloud_desc
          asg_options = buildOptionsHash

          need_tag_update = false
          oldtags = current.tags.map { |t|
            t.key+" "+t.value+" "+t.propagate_at_launch.to_s
          }
          tag_conf = { :tags => asg_options[:tags] }
          tag_conf[:tags].each { |t|
            if !oldtags.include?(t[:key]+" "+t[:value]+" "+t[:propagate_at_launch].to_s)
              need_tag_update = true
            end
            t[:resource_id] = @mu_name
            t[:resource_type] = "auto-scaling-group"
          }

          if need_tag_update
            MU.log "Updating ServerPool #{@mu_name} with new tags", MU::NOTICE, details: tag_conf[:tags]

            MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).create_or_update_tags(tag_conf)
            current.instances.each { |instance|
              tag_conf[:tags].each { |t|
                MU::Cloud::AWS.createTag(instance.instance_id, t[:key], t[:value], region: @region, credentials: @credentials)
              }
            }
          end

# XXX actually compare for changes instead of just blindly updating

          asg_options.delete(:tags)
          asg_options[:min_size] = @config["min_size"]
          asg_options[:max_size] = @config["max_size"]
          asg_options[:new_instances_protected_from_scale_in] = (@config['scale_in_protection'] == "all")
          if asg_options[:target_group_arns]
            MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).attach_load_balancer_target_groups(
              auto_scaling_group_name: @mu_name,
              target_group_arns: asg_options[:target_group_arns]
            )
            asg_options.delete(:target_group_arns)
          end

          MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group(asg_options)

          if @config['scale_in_protection']
            if @config['scale_in_protection'] == "all"
              setScaleInProtection(listNodes.size)
            elsif @config['scale_in_protection'] == "initial"
              setScaleInProtection(@config['min_size'])
            elsif @config['scale_in_protection'].match(/^\d+$/)
              setScaleInProtection(@config['scale_in_protection'].to_i)
            end
          else
            setScaleInProtection(0)
          end

          ext_pols = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_policies(
            auto_scaling_group_name: @mu_name
          ).scaling_policies
          if @config["scaling_policies"] and @config["scaling_policies"].size > 0
            legit_policies = []
            @config["scaling_policies"].each { |policy|
              legit_policies << @deploy.getResourceName("#{@config['name']}-#{policy['name']}")
            }
            # Delete any scaling policies we're not configured for
            ext_pols.each { |ext|
              if !legit_policies.include?(ext.policy_name)
                MU.log "Scaling policy #{ext.policy_name} is not named in scaling_policies, removing from #{@mu_name}", MU::NOTICE, details: ext
                MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_policy(
                  auto_scaling_group_name: @mu_name,
                  policy_name: ext.policy_name
                )
              end
            }

            @config["scaling_policies"].each { |policy|
              policy_name = @deploy.getResourceName("#{@config['name']}-#{policy['name']}")
              policy_params = {
                :auto_scaling_group_name => @mu_name,
                :policy_name => policy_name,
                :policy_type => policy['policy_type']
              }

              if policy["policy_type"] == "SimpleScaling"
                policy_params[:cooldown] = policy['cooldown']
                policy_params[:scaling_adjustment] = policy['adjustment']
                policy_params[:adjustment_type] = policy['type']
              elsif policy["policy_type"] == "TargetTrackingScaling"
                policy_params[:target_tracking_configuration] = MU.strToSym(policy['target_tracking_configuration'])
                policy_params[:target_tracking_configuration].delete(:preferred_target_group)
                if policy_params[:target_tracking_configuration][:predefined_metric_specification] and
                   policy_params[:target_tracking_configuration][:predefined_metric_specification][:predefined_metric_type] == "ALBRequestCountPerTarget"
                  lb = @deploy.deployment["loadbalancers"].values.first
                  if @deploy.deployment["loadbalancers"].size > 1
                    MU.log "Multiple load balancers attached to Autoscale group #{@mu_name}, guessing wildly which one to use for TargetTrackingScaling policy", MU::WARN
                  end
                  lb_path = if lb["targetgroups"].size > 1
                    if policy['target_tracking_configuration']["preferred_target_group"] and
                       lb["targetgroups"][policy['target_tracking_configuration']["preferred_target_group"]]
                      lb["arn"].split(/:/)[5].sub(/^loadbalancer\//, "")+"/"+lb["targetgroups"][policy['target_tracking_configuration']["preferred_target_group"]].split(/:/)[5]
                    else
                      if policy['target_tracking_configuration']["preferred_target_group"]
                        MU.log "preferred_target_group was set to '#{policy["preferred_target_group"]}' but I don't see a target group by that name", MU::WARN
                      end
                      MU.log "Multiple target groups attached to Autoscale group #{@mu_name}, guessing wildly which one to use for TargetTrackingScaling policy", MU::WARN, details: lb["targetgroups"].keys
                      lb["arn"].split(/:/)[5].sub(/^loadbalancer\//, "")+"/"+lb["targetgroups"].values.first.split(/:/)[5]
                    end
                  end

                  policy_params[:target_tracking_configuration][:predefined_metric_specification][:resource_label] = lb_path
                end
                policy_params[:estimated_instance_warmup] = policy['estimated_instance_warmup']
              elsif policy["policy_type"] == "StepScaling"
                step_adjustments = []
                policy['step_adjustments'].each{|step|
                  step_adjustments << {:metric_interval_lower_bound => step["lower_bound"], :metric_interval_upper_bound => step["upper_bound"], :scaling_adjustment => step["adjustment"]}
                }
                policy_params[:metric_aggregation_type] = policy['metric_aggregation_type']
                policy_params[:step_adjustments] = step_adjustments
                policy_params[:estimated_instance_warmup] = policy['estimated_instance_warmup']
                policy_params[:adjustment_type] = policy['type']
              end

              policy_params[:min_adjustment_magnitude] = policy['min_adjustment_magnitude'] if !policy['min_adjustment_magnitude'].nil?

              policy_already_correct = false
              ext_pols.each { |ext|
                if ext.policy_name == policy_name
                  if !MU.hashCmp(MU.structToHash(ext), policy_params, missing_is_default: true)
                    MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_policy(
                      auto_scaling_group_name: @mu_name,
                      policy_name: policy_name
                    )
                  else
                    policy_already_correct = true
                  end
                  break
                end
              }
              if !policy_already_correct
                MU.log "Putting scaling policy #{policy_name} for #{@mu_name}", MU::NOTICE, details: policy_params
                MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).put_scaling_policy(policy_params)
              end

            }
          end

        end

        @cloud_desc_cache = nil
        # Retrieve the AWS descriptor for this Autoscale group
        # @return [OpenStruct]
        def cloud_desc(use_cache: true)
          return @cloud_desc_cache if @cloud_desc_cache and use_cache
          return nil if !@cloud_id
          @cloud_desc_cache = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_auto_scaling_groups(
            auto_scaling_group_names: [@mu_name]
          ).auto_scaling_groups.first
          @cloud_desc_cache
        end

        # Canonical Amazon Resource Number for this resource
        # @return [String]
        def arn
          cloud_desc.auto_scaling_group_arn
        end

        # Retrieve deployment metadata for this Autoscale group
        # @return [Hash]
        def notify
          return MU.structToHash(cloud_desc)
        end

        # Locate an existing ServerPool or ServerPools 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 ServerPools
        def self.find(**args)
          found = {}

          if args[:cloud_id]
            resp = MU::Cloud::AWS.autoscale(region: args[:region], credentials: args[:credentials]).describe_auto_scaling_groups({
              auto_scaling_group_names: [
                args[:cloud_id]
              ], 
            })
            resp.auto_scaling_groups.each { |asg|
              found[asg.auto_scaling_group_name] = asg
            }
          elsif args[:instance_id]
            # try to reverse map from an instance id to an autoscale group
            resp = MU::Cloud::AWS.autoscale(region: args[:region], credentials: args[:credentials]).describe_auto_scaling_instances(instance_ids: [args[:instance_id]])
            if resp and resp.auto_scaling_instances
              asg_names = resp.auto_scaling_instances.map { |g|
                g.auto_scaling_group_name
              }.uniq
              asg_names.each { |asg_name|
                found.merge!(find(cloud_id: asg_name, credentials: args[:credentials], region: args[:region]))
              }
            end
          else
            next_token = nil
            begin
              resp = MU::Cloud::AWS.autoscale(region: args[:region], credentials: args[:credentials]).describe_auto_scaling_groups
              next_token = resp.next_token
              resp.auto_scaling_groups.each { |asg|
                found[asg.auto_scaling_group_name] = asg
              }
            end while next_token
          end

# TODO implement the tag-based search
          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(**_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

        if cloud_desc.tags and !cloud_desc.tags.empty?
          cloud_desc.tags.each { |tag|
            bok['tags'] ||= []
            bok['tags'] << { "key" => tag.key, "value" => tag.value }
          }
          realname = MU::Adoption.tagsToName(bok['tags'], basename: @cloud_id)
          if realname
            bok['name'] = realname
            bok['name'].gsub!(/[^a-zA-Z0-9_\-]/, "_")
          end
        end
        bok['name'] ||= @cloud_id

        bok['min_size'] = cloud_desc.min_size
        bok['max_size'] = cloud_desc.max_size

        if cloud_desc.launch_configuration_name
          launch = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_launch_configurations(
            launch_configuration_names: [cloud_desc.launch_configuration_name]
          ).launch_configurations.first
          bok['basis'] = {
            "launch_config" => {
              "image_id" => launch.image_id,
              "name" => bok['name'],
              "size" => launch.instance_type
            }
          }
        end

        if cloud_desc.vpc_zone_identifier and
           !cloud_desc.vpc_zone_identifier.empty?
          nets = cloud_desc.vpc_zone_identifier.split(/,/)
          begin
            resp = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_subnets(subnet_ids: nets).subnets.first
            bok['vpc'] = MU::Config::Ref.get(
              id: resp.vpc_id,
              cloud: "AWS",
              credentials: @credentials,
              type: "vpcs",
              subnets: nets.map { |s| { "subnet_id" => s } }
            )
          rescue Aws::EC2::Errors::InvalidSubnetIDNotFound => e
            if e.message.match(/The subnet ID '(subnet-[a-f0-9]+)' does not exist/)
              nets.delete(Regexp.last_match[1])
              if nets.empty?
                MU.log "Autoscale Group #{@cloud_id} was configured for a VPC, but the configuration held no valid subnets", MU::WARN, details: cloud_desc.vpc_zone_identifier.split(/,/)
              end
            else
              raise e
            end
          end
        end

#        MU.log @cloud_id, MU::NOTICE, details: cloud_desc

        bok
      end


        # 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
        def self.schema(_config)
          toplevel_required = []

          term_policies = MU::Cloud::AWS.credConfig ? MU::Cloud::AWS.autoscale.describe_termination_policy_types.termination_policy_types : ["AllocationStrategy", "ClosestToNextInstanceHour", "Default", "NewestInstance", "OldestInstance", "OldestLaunchConfiguration", "OldestLaunchTemplate"]
          
          schema = {
            "role_strip_path" => {
              "type" => "boolean",
              "default" => false,
              "description" => "Normally we namespace IAM roles with a +path+ set to match our +deploy_id+; this disables that behavior. Temporary workaround for a bug in EKS/IAM integration."
            },
            "notifications" => {
              "type" => "object",
              "description" => "Send notifications to an SNS topic for basic AutoScaling events",
              "properties" => {
                "topic" => {
                  "type" => "string",
                  "description" => "The short name or ARN of an SNS topic which should receive notifications for basic Autoscaling events"
                },
               "events" => {
                  "type" => "array",
                  "description" => "The AutoScaling events which should generate a notification",
                  "items" => {
                    "type" => "string",
                    "description" => "The AutoScaling events which should generate a notification",
                    "enum" => ["launch", "failed_launch", "terminate", "failed_terminate"]
                  },
                  "default" => ["launch", "failed_launch", "terminate", "failed_terminate"]
                }
              }
            },
            "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.",
            },
            "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"
              }
            },
            "canned_iam_policies" => {
              "type" => "array",
              "items" => {
                "description" => "IAM policies to attach, pre-defined by Amazon (e.g. AmazonEKSWorkerNodePolicy)",
                "type" => "string"
              }
            },
            "schedule" => {
              "type" => "array",
              "items" => {
                "type" => "object",
                "required" => ["action_name"],
                "description" => "Tell AutoScale to alter min/max/desired for this group at a scheduled time, optionally repeating.",
                "properties" => {
                  "action_name" => {
                    "type" => "string",
                    "description" => "A name for this scheduled action, e.g. 'scale-down-over-night'"
                  },
                  "start_time" => {
                    "type" => "string",
                    "description" => "When should this one-off scheduled behavior take effect? Times are UTC. Must be a valid Ruby Time.parse() string, e.g. '20:00' or '2014-05-12T08:00:00Z'. If declared along with 'recurrence,' AutoScaling performs the action at this time, and then performs the action based on the specified recurrence."
                  },
                  "end_time" => {
                    "type" => "string",
                    "description" => "When should this scheduled behavior end? Times are UTC. Must be a valid Ruby Time.parse() string, e.g. '20:00' or '2014-05-12T08:00:00Z'"
                  },
                  "recurrence" => {
                    "type" => "string",
                    "description" => "A recurring schedule for this action, in Unix cron syntax format (e.g. '0 20 * * *'). Times are UTC."
                  },
                  "min_size" => {"type" => "integer"},
                  "max_size" => {"type" => "integer"},
                  "desired_capacity" => {
                    "type" => "integer",
                    "description" => "The number of Amazon EC2 instances that should be running in the group. Should be between min_size and max_size."
                  },

                }
              }
            },
            "scale_in_protection" => {
              "type" => "string",
              "description" => "Protect instances from scale-in termination. Can be 'all', 'initial' (essentially 'min_size'), or an number; note the number needs to be a string, so put it in quotes",
              "pattern" => "^(all|initial|\\d+)$"
            },
            "scale_with_alb_traffic" => {
              "type" => "float",
              "description" => "Shorthand for creating a target_tracking_configuration to scale on ALBRequestCountPerTarget with some reasonable defaults"
            },
            "scale_with_cpu" => {
              "type" => "float",
              "description" => "Shorthand for creating a target_tracking_configuration to scale on ASGAverageCPUUtilization with some reasonable defaults"
            },
            "scale_with_network_in" => {
              "type" => "float",
              "description" => "Shorthand for creating a target_tracking_configuration to scale on ASGAverageNetworkIn with some reasonable defaults"
            },
            "scale_with_network_out" => {
              "type" => "float",
              "description" => "Shorthand for creating a target_tracking_configuration to scale on ASGAverageNetworkOut with some reasonable defaults"
            },
            "termination_policies" => {
              "type" => "array",
              "minItems" => 1,
              "items" => {
                "type" => "String",
                "default" => "Default",
                "enum" => term_policies
              }
            },
            "scaling_policies" => {
              "type" => "array",
              "minItems" => 1,
              "items" => {
                "type" => "object",
                "required" => ["name"],
                "additionalProperties" => false,
                "description" => "A custom AWS Autoscale scaling policy for this pool.",
                "properties" => {
                  "name" => {
                    "type" => "string"
                  },
                  "alarms" => MU::Config::Alarm.inline,
                  "type" => {
                    "type" => "string",
                    "enum" => ["ChangeInCapacity", "ExactCapacity", "PercentChangeInCapacity"],
                    "description" => "Specifies whether 'adjustment' is an absolute number or a percentage of the current capacity for SimpleScaling and StepScaling. Valid values are ChangeInCapacity, ExactCapacity, and PercentChangeInCapacity."
                  },
                  "adjustment" => {
                    "type" => "integer",
                    "description" => "The number of instances by which to scale. 'type' determines the interpretation of this number (e.g., as an absolute number or as a percentage of the existing Auto Scaling group size). A positive increment adds to the current capacity and a negative value removes from the current capacity. Used only when policy_type is set to 'SimpleScaling'"
                  },
                  "cooldown" => {
                    "type" => "integer",
                    "default" => 1,
                    "description" => "The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start."
                  },
                  "min_adjustment_magnitude" => {
                    "type" => "integer",
                    "description" => "Used when 'type' is set to 'PercentChangeInCapacity', the scaling policy changes the DesiredCapacity of the Auto Scaling group by at least the number of instances specified in the value."
                  },
                  "policy_type" => {
                    "type" => "string",
                    "enum" => ["SimpleScaling", "StepScaling", "TargetTrackingScaling"],
                    "description" => "'StepScaling' will add capacity based on the magnitude of the alarm breach, 'SimpleScaling' will add capacity based on the 'adjustment' value provided. Defaults to 'SimpleScaling'.",
                    "default" => "SimpleScaling"
                  },
                  "metric_aggregation_type" => {
                    "type" => "string",
                    "enum" => ["Minimum", "Maximum", "Average"],
                    "description" => "Defaults to 'Average' if not specified. Required when policy_type is set to 'StepScaling'",
                    "default" => "Average"
                  },
                  "step_adjustments" => {
                    "type" => "array",
                    "minItems" => 1,
                    "items" => {
                      "type" => "object",
                      "title" => "admin",
                      "description" => "Requires policy_type 'StepScaling'",
                      "required" => ["adjustment"],
                      "additionalProperties" => false,
                      "properties" => {
                        "adjustment" => {
                          "type" => "integer",
                          "description" => "The number of instances by which to scale at this specific step. Postive value when adding capacity, negative value when removing capacity"
                        },
                        "lower_bound" => {
                          "type" => "integer",
                          "description" => "The lower bound value in percentage points above/below the alarm threshold at which to add/remove capacity for this step. Positive value when adding capacity and negative when removing capacity. If this is the first step and capacity is being added this value will most likely be 0"
                        },
                        "upper_bound" => {
                          "type" => "integer",
                          "description" => "The upper bound value in percentage points above/below the alarm threshold at which to add/remove capacity for this step. Positive value when adding capacity and negative when removing capacity. If this is the first step and capacity is being removed this value will most likely be 0"
                        }
                      }
                    }
                  },
                  "estimated_instance_warmup" => {
                    "type" => "integer",
                    "description" => "Required when policy_type is set to 'StepScaling'"
                  },
                  "target_tracking_configuration" => {
                    "type" => "object",
                    "description" => "Required when policy_type is set to 'TargetTrackingScaling' https://docs.aws.amazon.com/sdkforruby/api/Aws/AutoScaling/Types/TargetTrackingConfiguration.html",
                    "required" => ["target_value"],
                    "additionalProperties" => false,
                    "properties" => {
                      "target_value" => {
                        "type" => "float",
                        "description" => "The target value for the metric."
                      },
                      "preferred_target_group" => {
                        "type" => "string",
                        "description" => "If our load balancer has multiple target groups, prefer the one with this name instead of choosing one arbitrarily"
                      },
                      "disable_scale_in" => {
                        "type" => "boolean",
                        "description" => "If set to true, new instances created by this policy will not be subject to termination by scaling in.",
                        "default" => false
                      },
                      "predefined_metric_specification" => {
                        "description" => "A predefined metric. You can specify either a predefined metric or a customized metric. https://docs.aws.amazon.com/sdkforruby/api/Aws/AutoScaling/Types/PredefinedMetricSpecification.html",
                        "type" => "string",
                        "enum" => ["ASGAverageCPUUtilization", "ASGAverageNetworkIn", "ASGAverageNetworkOut", "ALBRequestCountPerTarget"],
                        "default" => "ASGAverageCPUUtilization"
                      },
                      "customized_metric_specification" => {
                        "type" => "object",
                        "description" => "A customized metric. You can specify either a predefined metric or a customized metric. https://docs.aws.amazon.com/sdkforruby/api/Aws/AutoScaling/Types/TargetTrackingConfiguration.html#customized_metric_specification-instance_method",
                        "additionalProperties" => false,
                        "required" => ["metric_name", "namespace", "statistic"],
                        "properties" => {
                          "metric_name" => {
                            "type" => "string",
                            "description" => "The name of the attribute to monitor eg. CPUUtilization."
                          },
                          "namespace" => {
                            "type" => "string",
                            "description" => "The name of container 'metric_name' belongs to eg. 'AWS/ApplicationELB'"
                          },
                          "statistic" => {
                            "type" => "string",
                            "enum" => ["Average", "Minimum", "Maximum", "SampleCount", "Sum"]
                          },
                          "unit" => {
                            "type" => "string",
                            "description" => "Associated with the 'metric', usually something like Megabits or Seconds"
                          },
                          "dimensions" => {
                            "type" => "array",
                            "description" => "What resource to monitor with the alarm we are implicitly declaring",
                            "items" => {
                              "type" => "object",
                              "additionalProperties" => false,
                              "required" => ["name", "value"],
                              "description" => "What resource to monitor with the alarm we are implicitly declaring",
                              "properties" => {
                                "name" => {
                                  "type" => "string",
                                  "description" => "The type of resource we're monitoring, e.g. InstanceId or AutoScalingGroupName"
                                },
                                "value" => {
                                  "type" => "string",
                                  "description" => "The name or cloud identifier of the resource we're monitoring"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "ingress_rules" => MU::Cloud.resourceClass("AWS", "FirewallRule").ingressRuleAddtlSchema
          }
          [toplevel_required, schema]
        end

        # Cloud-specific pre-processing of {MU::Config::BasketofKittens::server_pools}, bare and unvalidated.
        # @param pool [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
        def self.validateConfig(pool, configurator)
          ok = true

          if pool["termination_policy"]
            valid_policies = MU::Cloud::AWS.autoscale(region: pool['region']).describe_termination_policy_types.termination_policy_types
            if !valid_policies.include?(pool["termination_policy"])
              ok = false
              MU.log "Termination policy #{pool["termination_policy"]} is not valid in region #{pool['region']}", MU::ERR, details: valid_policies
            end
          end

          if !pool["schedule"].nil?
            pool["schedule"].each { |s|
              if !s['min_size'] and !s['max_size'] and !s['desired_capacity']
                MU.log "Scheduled action for AutoScale group #{pool['name']} must declare at least one of min_size, max_size, or desired_capacity", MU::ERR
                ok = false
              end
              if !s['start_time'] and !s['recurrence']
                MU.log "Scheduled action for AutoScale group #{pool['name']} must declare at least one of start_time or recurrence", MU::ERR
                ok = false
              end
              ['start_time', 'end_time'].each { |time|
                next if !s[time]
                begin
                  Time.parse(s[time])
                rescue StandardError => e
                  MU.log "Failed to parse #{time} '#{s[time]}' in scheduled action for AutoScale group #{pool['name']}: #{e.message}", MU::ERR
                  ok = false
                end
              }
              if s['recurrence'] and !s['recurrence'].match(/^\s*[\d\-\*]+\s+[\d\-\*]+\s[\d\-\*]+\s[\d\-\*]+\s[\d\-\*]\s*$/)
                MU.log "Failed to parse recurrence '#{s['recurrence']}' in scheduled action for AutoScale group #{pool['name']}: does not appear to be a valid cron string", MU::ERR
                ok = false
              end
            }
          end

          scale_aliases = {
            "scale_with_alb_traffic" => "ALBRequestCountPerTarget",
            "scale_with_cpu" => "ASGAverageCPUUtilization",
            "scale_with_network_in" => "ASGAverageNetworkIn",
            "scale_with_network_out" => "ASGAverageNetworkOut"
          }

          scale_aliases.keys.each { |sp|
            if pool[sp]
              pool['scaling_policies'] ||= []
              pool['scaling_policies'] << {
                'name' => scale_aliases[sp],
                'adjustment' => 1,
                'policy_type' => "TargetTrackingScaling",
                'estimated_instance_warmup' => 60,
                'target_tracking_configuration' => {
                  'target_value' => pool[sp],
                  'predefined_metric_specification' => scale_aliases[sp]
                }
              }
            end
          }

          if !pool["basis"]["launch_config"].nil?
            launch = pool["basis"]["launch_config"]
            launch['iam_policies'] ||= pool['iam_policies']

            launch['size'] = MU::Cloud.resourceClass("AWS", "Server").validateInstanceType(launch["size"], pool["region"])
            ok = false if launch['size'].nil?
            if !launch['generate_iam_role']
              if !launch['iam_role'] and pool['cloud'] != "CloudFormation"
                MU.log "Must set iam_role if generate_iam_role set to false", MU::ERR
                ok = false
              end
              if !launch['iam_policies'].nil? and launch['iam_policies'].size > 0
                MU.log "Cannot mix iam_policies with generate_iam_role set to false", MU::ERR
                ok = false
              end
            end

            ["generate_iam_role", "iam_role", "canned_iam_policies", "iam_policies"].each { |key|
              pool[key] = launch[key] if !launch[key].nil?
            }
            MU::Cloud.resourceClass("AWS", "Server").generateStandardRole(pool, configurator)

            launch["ami_id"] ||= launch["image_id"]
            if launch["server"].nil? and launch["instance_id"].nil? and launch["ami_id"].nil?
              img_id = MU::Cloud.getStockImage("AWS", platform: pool['platform'], region: pool['region'])
              if img_id
                launch['ami_id'] = configurator.getTail("pool"+pool['name']+"AMI", value: img_id, prettyname: "pool"+pool['name']+"AMI", cloudtype: "AWS::EC2::Image::Id")
  
              else
                ok = false
                MU.log "One of the following MUST be specified for launch_config: server, ami_id, instance_id.", MU::ERR
              end
            end
            if launch["server"] != nil
              MU::Config.addDependency(pool, launch["server"], "server", their_phase: "groom")
# XXX I dunno, maybe toss an error if this isn't done already
#              servers.each { |server|
#                if server["name"] == launch["server"]
#                  server["create_ami"] = true
#                end
#              }
            end
          end
  
          if !pool["scaling_policies"].nil?
            pool["scaling_policies"].each { |policy|
              if policy['type'] != "PercentChangeInCapacity" and !policy['min_adjustment_magnitude'].nil?
                MU.log "Cannot specify scaling policy min_adjustment_magnitude if type is not PercentChangeInCapacity", MU::ERR
                ok = false
              end
  
              if policy["policy_type"] == "SimpleScaling"
                unless policy["cooldown"] && policy["adjustment"]
                  MU.log "You must specify 'cooldown' and 'adjustment' when 'policy_type' is set to 'SimpleScaling'", MU::ERR
                  ok = false
                end
                unless policy['type']
                  MU.log "You must specify a 'type' when 'policy_type' is set to 'SimpleScaling'", MU::ERR
                  ok = false
                end
              elsif policy["policy_type"] == "TargetTrackingScaling"
                unless policy["target_tracking_configuration"]
                  MU.log "You must specify 'target_tracking_configuration' when 'policy_type' is set to 'TargetTrackingScaling'", MU::ERR
                  ok = false
                end
                unless policy["target_tracking_configuration"]["customized_metric_specification"] or
                       policy["target_tracking_configuration"]["predefined_metric_specification"]
                  MU.log "Your target_tracking_configuration must specify one of customized_metric_specification or predefined_metric_specification when 'policy_type' is set to 'TargetTrackingScaling'", MU::ERR
                  ok = false
                end
                # we gloss over an annoying layer of indirection in the API here
                if policy["target_tracking_configuration"]["predefined_metric_specification"]
                  policy["target_tracking_configuration"]["predefined_metric_specification"] = {
                    "predefined_metric_type" => policy["target_tracking_configuration"]["predefined_metric_specification"]
                  }
                end
              elsif policy["policy_type"] == "StepScaling"
                if policy["step_adjustments"].nil? || policy["step_adjustments"].empty?
                  MU.log "You must specify 'step_adjustments' when 'policy_type' is set to 'StepScaling'", MU::ERR
                  ok = false
                end
                unless policy['type']
                  MU.log "You must specify a 'type' when 'policy_type' is set to 'StepScaling'", MU::ERR
                  ok = false
                end
  
                policy["step_adjustments"].each{ |step|
                  if step["adjustment"].nil?
                    MU.log "You must specify 'adjustment' for 'step_adjustments' when 'policy_type' is set to 'StepScaling'", MU::ERR
                    ok = false
                  end
  
                  if step["adjustment"] >= 1 && policy["estimated_instance_warmup"].nil?
                    MU.log "You must specify 'estimated_instance_warmup' when 'policy_type' is set to 'StepScaling' and adding capacity", MU::ERR
                    ok = false
                  end
  
                  if step["lower_bound"].nil? && step["upper_bound"].nil?
                    MU.log "You must specify 'lower_bound' and/or upper_bound for 'step_adjustments' when 'policy_type' is set to 'StepScaling'", MU::ERR
                    ok = false
                  end
                }
              end
  
              if policy["alarms"] && !policy["alarms"].empty?
                policy["alarms"].each { |alarm|
                  alarm["name"] = "scaling-policy-#{pool["name"]}-#{alarm["name"]}"
                  alarm["cloud"] = "AWS",
                  alarm['dimensions'] = [] if !alarm['dimensions']
                  alarm['dimensions'] << { "name" => pool["name"], "cloud_class" => "AutoScalingGroupName" }
                  alarm["namespace"] = "AWS/EC2" if alarm["namespace"].nil?
                  alarm['cloud'] = pool['cloud']
                  alarm['credentials'] = pool['credentials']
                  alarm['region'] = pool['region']
                  ok = false if !configurator.insertKitten(alarm, "alarms")
                }
              end
            }
          end
          ok
        end

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

        # Denote whether this resource implementation is experiment, ready for
        # testing, or ready for production use.
        def self.quality
          MU::Cloud::RELEASE
        end

        # Remove all autoscale groups associated with the currently loaded deployment.
        # @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]
        def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
          MU.log "AWS::ServerPool.cleanup: need to support flags['known']", MU::DEBUG, details: flags

          filters = [{name: "key", values: ["MU-ID"]}]
          if !ignoremaster
            filters << {name: "key", values: ["MU-MASTER-IP"]}
          end
          resp = MU::Cloud::AWS.autoscale(credentials: credentials, region: region).describe_tags(
            filters: filters,
            max_records: 100
          )

          return nil if resp.tags.nil? or resp.tags.size == 0

          maybe_purge = []
          no_purge = []
          resp.data.tags.each { |asg|
            if asg.resource_type != "auto-scaling-group"
              no_purge << asg.resource_id
            end
            if asg.key == "MU-MASTER-IP" and asg.value != MU.mu_public_ip and !ignoremaster
              no_purge << asg.resource_id
            end
            if asg.key == "MU-ID" and asg.value == deploy_id
              maybe_purge << asg.resource_id
            end
          }


          maybe_purge.each { |resource_id|
            next if no_purge.include?(resource_id)
            MU.log "Removing AutoScale group #{resource_id}"
            next if noop
            retries = 0
            begin
              MU::Cloud::AWS.autoscale(credentials: credentials, region: region).delete_auto_scaling_group(
                  auto_scaling_group_name: resource_id,
                  # XXX this should obey @force
                  force_delete: true
              )
            rescue Aws::AutoScaling::Errors::InternalFailure => e
              if retries < 5
                MU.log "Got #{e.inspect} while removing AutoScale group #{resource_id}.", MU::WARN
                sleep 10
                retry
              else
                MU.log "Failed to delete AutoScale group #{resource_id}", MU::ERR
              end
            end

#            MU::Cloud.resourceClass("AWS", "Server").removeIAMProfile(resource_id)

            # Generally there should be a launch_configuration of the same name
            # XXX search for these independently, too?
            retries = 0
            begin
              MU.log "Removing AutoScale Launch Configuration #{resource_id}"
              MU::Cloud::AWS.autoscale(credentials: credentials, region: region).delete_launch_configuration(
                launch_configuration_name: resource_id
              )
            rescue Aws::AutoScaling::Errors::ValidationError => e
              MU.log "No such Launch Configuration #{resource_id}"
            rescue Aws::AutoScaling::Errors::InternalFailure => e
              if retries < 5
                MU.log "Got #{e.inspect} while removing Launch Configuration #{resource_id}.", MU::WARN
                sleep 10
                retry
              else
                MU.log "Failed to delete Launch Configuration #{resource_id}", MU::ERR
              end
            end
          }
          return nil
        end

        private

        def createUpdateLaunchConfig
          return if !@config['basis'] or !@config['basis']["launch_config"]

          instance_secret = Password.random(50)
          @deploy.saveNodeSecret("default", instance_secret, "instance_secret")

          if !@config['basis']['launch_config']["server"].nil?
            #XXX this isn't how we find these; use findStray or something
            if @deploy.deployment["images"].nil? or @deploy.deployment["images"][@config['basis']['launch_config']["server"]].nil?
              raise MuError, "#{@mu_name} needs an AMI from server #{@config['basis']['launch_config']["server"]}, but I don't see one anywhere"
            end
            @config['basis']['launch_config']["ami_id"] = @deploy.deployment["images"][@config['basis']['launch_config']["server"]]["image_id"]
            MU.log "Using AMI '#{@config['basis']['launch_config']["ami_id"]}' from sibling server #{@config['basis']['launch_config']["server"]} in ServerPool #{@mu_name}"
          elsif !@config['basis']['launch_config']["instance_id"].nil?
            @config['basis']['launch_config']["ami_id"] = MU::Cloud.resourceClass("AWS", "Server").createImage(
              name: @mu_name,
              instance_id: @config['basis']['launch_config']["instance_id"],
              credentials: @credentials,
              region: @region
            )[@region]
          end
          MU::Cloud.resourceClass("AWS", "Server").waitForAMI(@config['basis']['launch_config']["ami_id"].to_s, credentials: @credentials)

          oldlaunch = MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).describe_launch_configurations(
            launch_configuration_names: [@mu_name]
          ).launch_configurations.first

          userdata = 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.chef_user,
              "publicIP" => MU.mu_public_ip,
              "mommaCatPort" => MU.mommaCatPort,
              "chefVersion" => MU.chefVersion,
              "adminBucketName" => MU::Cloud::AWS.adminBucketName(@credentials),
              "windowsAdminName" => @config['windows_admin_username'],
              "skipApplyUpdates" => @config['skipinitialupdates'],
              "resourceName" => @config["name"],
              "resourceType" => "server_pool",
              "platform" => @config["platform"]
            },
            custom_append: @config['userdata_script']
          )

          # Figure out which devices are embedded in the AMI already.
          image = MU::Cloud::AWS.ec2.describe_images(image_ids: [@config["basis"]["launch_config"]["ami_id"]]).images.first

          if image.nil?
            raise "#{@config["basis"]["launch_config"]["ami_id"]} does not exist, cannot update/create launch config #{@mu_name}"
          end

          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)
                if ext_disks[disk.device_name].has_key?(:snapshot_id)
                  ext_disks[disk.device_name].delete(:encrypted)
                end
              end
            }
          end

          storage = []
          if !@config["basis"]["launch_config"]["storage"].nil?
            @config["basis"]["launch_config"]["storage"].each { |vol|
              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.resourceClass("AWS", "Server").convertBlockDeviceMapping(vol)
              storage << mapping
            }
          end

          storage.concat(MU::Cloud.resourceClass("AWS", "Server").ephemeral_mappings)

          if !oldlaunch.nil?
            olduserdata = Base64.decode64(oldlaunch.user_data)
            if userdata == olduserdata and
                oldlaunch.image_id == @config["basis"]["launch_config"]["ami_id"] and
                oldlaunch.ebs_optimized == @config["basis"]["launch_config"]["ebs_optimized"] and
                oldlaunch.instance_type == @config["basis"]["launch_config"]["size"] and
                oldlaunch.instance_monitoring.enabled == @config["basis"]["launch_config"]["monitoring"]
                # XXX check more things
#                launch.block_device_mappings != storage
#                XXX block device comparison isn't this simple
              return
            end

            # Put our Autoscale group onto a temporary launch config
            begin

              MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).create_launch_configuration(
                launch_configuration_name: @mu_name+"-TMP",
                user_data: Base64.encode64(olduserdata),
                image_id: oldlaunch.image_id,
                key_name: oldlaunch.key_name,
                security_groups: oldlaunch.security_groups,
                instance_type: oldlaunch.instance_type,
                block_device_mappings: storage,
                instance_monitoring: oldlaunch.instance_monitoring,
                iam_instance_profile: oldlaunch.iam_instance_profile,
                ebs_optimized: oldlaunch.ebs_optimized,
                associate_public_ip_address: oldlaunch.associate_public_ip_address
              )
            rescue ::Aws::AutoScaling::Errors::ValidationError => e
              if e.message.match(/Member must have length less than or equal to (\d+)/)
                MU.log "Userdata script too long updating #{@mu_name} Launch Config (#{Base64.encode64(userdata).size.to_s}/#{Regexp.last_match[1]} bytes)", MU::ERR
              else
                MU.log "Error updating #{@mu_name} Launch Config", MU::ERR, details: e.message
              end
              raise e.message
            end


            MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group(
              auto_scaling_group_name: @mu_name,
              launch_configuration_name: @mu_name+"-TMP"
            )
            # ...now back to an identical one with the "real" name
            MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_launch_configuration(
              launch_configuration_name: @mu_name
            )
          end

          # Now to build the new one
          sgs = []
          if @dependencies.has_key?("firewall_rule")
            @dependencies['firewall_rule'].values.each { |sg|
              sgs << sg.cloud_id
            }
          end

          launch_options = {
            :launch_configuration_name => @mu_name,
            :user_data => Base64.encode64(userdata),
            :image_id => @config["basis"]["launch_config"]["ami_id"],
            :key_name => @deploy.ssh_key_name,
            :security_groups => sgs,
            :instance_type => @config["basis"]["launch_config"]["size"],
            :block_device_mappings => storage,
            :instance_monitoring => {:enabled => @config["basis"]["launch_config"]["monitoring"]},
            :ebs_optimized => @config["basis"]["launch_config"]["ebs_optimized"]
          }
          if @config["vpc"] or @config["vpc_zone_identifier"]
            launch_options[:associate_public_ip_address] = @config["associate_public_ip"]
          end
          ["kernel_id", "ramdisk_id", "spot_price"].each { |arg|
            if @config['basis']['launch_config'][arg]
              launch_options[arg.to_sym] = @config['basis']['launch_config'][arg]
            end
          }
          rolename = nil

          ['generate_iam_role', 'iam_policies', 'canned_iam_policies', 'iam_role'].each { |field|
            if !@config['basis']['launch_config'].nil?
              @config[field] = @config['basis']['launch_config'][field]
            else
              @config['basis']['launch_config'][field] = @config[field]
            end
          }

          @config['iam_role'] = @config['basis']['launch_config']['iam_role'] = launch_options[:iam_instance_profile] = MU::Cloud.resourceClass("AWS", "Server").getIAMProfile(
            @config['name'],
            @deploy,
            generated: @config['basis']['launch_config']['generate_iam_role'],
            role_name: @config['basis']['launch_config']['iam_role'],
            region: @region,
            credentials: @credentials
          ).values.first

          lc_attempts = 0
          begin
            MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).create_launch_configuration(launch_options)
          rescue Aws::AutoScaling::Errors::ValidationError => e
            if lc_attempts > 3
              MU.log "Got error while creating #{@mu_name} Launch Config#{@credentials ? " with credentials #{@credentials}" : ""}: #{e.message}, retrying in 10s", MU::WARN, details: launch_options.reject { |k,_v | k == :user_data }
            end
            sleep 5
            lc_attempts += 1
            retry
          end

          if !oldlaunch.nil?
            # Tell the ASG to use the new one, and nuke the old one
            MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).update_auto_scaling_group(
              auto_scaling_group_name: @mu_name,
              launch_configuration_name: @mu_name
            )
            MU::Cloud::AWS.autoscale(region: @region, credentials: @credentials).delete_launch_configuration(
              launch_configuration_name: @mu_name+"-TMP"
            )
            MU.log "Launch Configuration #{@mu_name} replaced"
          else
            MU.log "Launch Configuration #{@mu_name} created"
          end

        end

        def buildOptionsHash
          asg_options = {
            :auto_scaling_group_name => @mu_name,
            :launch_configuration_name => @mu_name,
            :default_cooldown => @config["default_cooldown"],
            :health_check_type => @config["health_check_type"],
            :health_check_grace_period => @config["health_check_grace_period"],
            :tags => []
          }

          MU::MommaCat.listStandardTags.each_pair { |name, value|
            asg_options[:tags] << {key: name, value: value, propagate_at_launch: true}
          }

          if @config['optional_tags']
            MU::MommaCat.listOptionalTags.each_pair { |name, value|
              asg_options[:tags] << {key: name, value: value, propagate_at_launch: true}
            }
          end

          if @config['tags']
            @config['tags'].each { |tag|
              asg_options[:tags] << {key: tag['key'], value: tag['value'], propagate_at_launch: true}
            }
          end

          if @dependencies.has_key?("container_cluster")
            @dependencies['container_cluster'].values.each { |cc|
              if cc.config['flavor'] == "EKS"
                asg_options[:tags] << {
                  key: "kubernetes.io/cluster/#{cc.mu_name}",
                  value: "owned",
                  propagate_at_launch: true
                }
              end
            }
          end

          if @config["wait_for_nodes"] > 0
            asg_options[:min_size] = @config["wait_for_nodes"]
            asg_options[:max_size] = @config["wait_for_nodes"]
          else
            asg_options[:min_size] = @config["min_size"]
            asg_options[:max_size] = @config["max_size"]
          end

          if @config["loadbalancers"]
            lbs = []
            tg_arns = []
# XXX refactor this into the LoadBalancer resource
            @config["loadbalancers"].each { |lb|
              if lb["existing_load_balancer"]
                lbs << lb["existing_load_balancer"]
                @deploy.deployment["loadbalancers"] = Array.new if !@deploy.deployment["loadbalancers"]
                @deploy.deployment["loadbalancers"] << {
                    "name" => lb["existing_load_balancer"],
                    "awsname" => lb["existing_load_balancer"]
                    # XXX probably have to query API to get the DNS name of this one
                }
              elsif lb["concurrent_load_balancer"]
                lb = @deploy.findLitterMate(name: lb['concurrent_load_balancer'], type: "loadbalancers")
                raise MuError, "No loadbalancers exist! I need one named #{lb['concurrent_load_balancer']}" if !lb
                lbs << lb.mu_name
                if lb.targetgroups
                  tg_arns = lb.targetgroups.values.map { |tg| tg.target_group_arn }
                end
              end
            }
            if tg_arns.size > 0
              asg_options[:target_group_arns] = tg_arns
            end
            if lbs.size > 0
#              asg_options[:load_balancer_names] = lbs
            end
          end
          asg_options[:termination_policies] = @config["termination_policies"] if @config["termination_policies"]
          asg_options[:desired_capacity] = @config["desired_capacity"] if @config["desired_capacity"]

          if @config["vpc_zone_identifier"]
            asg_options[:vpc_zone_identifier] = @config["vpc_zone_identifier"]
          elsif @config["vpc"]
            if !@vpc and @config['vpc'].is_a?(MU::Config::Ref)
              @vpc = @config['vpc'].kitten
            end

            subnet_ids = []

            if !@vpc
              raise MuError, "Failed to load vpc for Autoscale Group #{@mu_name}"
            end
            if !@config["vpc"]["subnets"].nil? and @config["vpc"]["subnets"].size > 0
              @config["vpc"]["subnets"].each { |subnet|
                subnet_obj = @vpc.getSubnet(cloud_id: subnet["subnet_id"], name: subnet["subnet_name"])
                next if !subnet_obj
                subnet_ids << subnet_obj.cloud_id
              }
            else
              @vpc.subnets.each { |subnet_obj|
                next if subnet_obj.private? and ["all_public", "public"].include?(@config["vpc"]["subnet_pref"])
                next if !subnet_obj.private? and ["all_private", "private"].include?(@config["vpc"]["subnet_pref"])
                subnet_ids << subnet_obj.cloud_id
              }
            end
            if subnet_ids.size == 0
              raise MuError, "No valid subnets found for #{@mu_name} from #{@config["vpc"]}"
            end
            asg_options[:vpc_zone_identifier] = subnet_ids.join(",")
          end


          if @config['basis']["server"]
            srv_name = @config['basis']["server"]
# XXX cloudformation bits
            if @deploy.deployment['servers'] != nil and
                @deploy.deployment['servers'][srv_name] != nil
              asg_options[:instance_id] = @deploy.deployment['servers'][srv_name]["instance_id"]
            end
          elsif @config['basis']["instance_id"]
            # TODO should go fetch the name tag or something
# XXX cloudformation bits
            asg_options[:instance_id] = @config['basis']["instance_id"]
          end

          if !asg_options[:vpc_zone_identifier].nil? and asg_options[:vpc_zone_identifier].empty?
            asg_options.delete(:vpc_zone_identifier)
          end

          # Do the dance of specifying individual zones if we haven't asked to
          # use particular VPC subnets.
          if @config['zones'].nil? and asg_options[:vpc_zone_identifier].nil?
            @config["zones"] = MU::Cloud::AWS.listAZs(region: @region)
            MU.log "Using zones from #{@region}", MU::DEBUG, details: @config['zones']
          end
          asg_options[:availability_zones] = @config["zones"] if @config["zones"] != nil
          asg_options
        end

      end
    end
  end
end