class MU::Cloud::AWS::FirewallRule

A firewall ruleset as configured in {MU::Config::BasketofKittens::firewall_rules}

Public Class Methods

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

Remove all security groups (firewall rulesets) 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]

# File modules/mu/providers/aws/firewall_rule.rb, line 387
def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
  filters = if flags and flags["vpc_id"]
    [
      {name: "vpc-id", values: [flags["vpc_id"]]}
    ]
  else
    filters = [
      {name: "tag:MU-ID", values: [deploy_id]}
    ]
    if !ignoremaster
      filters << {name: "tag:MU-MASTER-IP", values: [MU.mu_public_ip]}
    end
    filters
  end

  # Some services create sneaky rogue ENIs which then block removal of
  # associated security groups. Find them and fry them.
  MU::Cloud.resourceClass("AWS", "VPC").purge_interfaces(noop, filters, region: region, credentials: credentials)

  resp = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_security_groups(
    filters: filters
  )

  resp.data.security_groups.each { |sg|
    MU.log "Revoking rules in EC2 Security Group #{sg.group_name} (#{sg.group_id})"

    if !noop
      revoke_rules(sg, region: region, credentials: credentials)
      revoke_rules(sg, egress: true, region: region, credentials: credentials)
    end
  }

  resp.data.security_groups.each { |sg|
    next if sg.group_name == "default"
    MU.log "Removing EC2 Security Group #{sg.group_name}"

    on_retry = Proc.new {
      # try to get out from under loose network interfaces with which
      # we're associated
      if sg.vpc_id
        default_sg = MU::Cloud.resourceClass("AWS", "VPC").getDefaultSg(sg.vpc_id, region: region, credentials: credentials)
        if default_sg
          eni_resp = MU::Cloud::AWS.ec2(credentials: credentials, region: region).describe_network_interfaces(
            filters: [ {name: "group-id", values: [sg.group_id]} ]
          )
          if eni_resp and eni_resp.data and
             eni_resp.data.network_interfaces
            eni_resp.data.network_interfaces.each { |iface|
              iface_groups = iface.groups.map { |if_sg| if_sg.group_id }
              iface_groups.delete(sg.group_id)
              iface_groups << default_sg if iface_groups.empty?
              MU.log "Attempting to remove #{sg.group_id} (#{sg.group_name}) from ENI #{iface.network_interface_id}"
              begin
                MU::Cloud::AWS.ec2(credentials: credentials, region: region).modify_network_interface_attribute(
                  network_interface_id: iface.network_interface_id,
                  groups: iface_groups
                )
              rescue ::Aws::EC2::Errors::InvalidNetworkInterfaceIDNotFound
                # fine by me
              rescue ::Aws::EC2::Errors::AuthFailure
                MU.log "Permission denied attempting to trim Security Group list for #{iface.network_interface_id}", MU::WARN, details: iface.groups.map { |g| g.group_name }.join(",")+" => default"
              end
            }
          end
        end
      end
    }

    if !noop
      MU.retrier([Aws::EC2::Errors::DependencyViolation, Aws::EC2::Errors::InvalidGroupInUse], ignoreme: [Aws::EC2::Errors::InvalidGroupNotFound], max: 10, wait: 10, on_retry: on_retry) {
        begin
          MU::Cloud::AWS.ec2(credentials: credentials, region: region).delete_security_group(group_id: sg.group_id)
        rescue Aws::EC2::Errors::CannotDelete => e
          MU.log e.message, MU::WARN
        end
      }
    end

  }
end
find(**args) click to toggle source

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

# File modules/mu/providers/aws/firewall_rule.rb, line 213
def self.find(**args)
  found = {}

  if !args[:cloud_id].nil? and !args[:cloud_id].empty?
    begin
      resp = MU::Cloud::AWS.ec2(region: args[:region], credentials: args[:credentials]).describe_security_groups(group_ids: [args[:cloud_id]])
      found[args[:cloud_id]] = resp.data.security_groups.first
    rescue ArgumentError => e
      MU.log "Attempting to load #{args[:cloud_id]}: #{e.inspect}", MU::WARN, details: caller
      return found
    rescue Aws::EC2::Errors::InvalidGroupNotFound => e
      MU.log "Attempting to load #{args[:cloud_id]}: #{e.inspect}", MU::DEBUG, details: caller
      return found
    end
  elsif !args[:tag_key].nil? and !args[:tag_value].nil?
    resp = MU::Cloud::AWS.ec2(region: args[:region], credentials: args[:credentials]).describe_security_groups(
      filters: [
        {name: "tag:#{args[:tag_key]}", values: [args[:tag_value]]}
      ]
    )
    if !resp.nil?
      resp.data.security_groups.each { |sg|
        found[sg.group_id] = sg
      }
    end
  else
    resp = MU::Cloud::AWS.ec2(region: args[:region], credentials: args[:credentials]).describe_security_groups
    resp.data.security_groups.each { |sg|
      found[sg.group_id] = sg
    }
  end

  found
end
getAssociatedInterfaces(sg_ids, credentials: nil, region: MU.curRegion) click to toggle source

Look up all the network interfaces using one or more security groups @param sg_ids [Array<String>] @param credentials [String] @param region [String] @return [Hash]

# File modules/mu/providers/aws/firewall_rule.rb, line 678
        def self.getAssociatedInterfaces(sg_ids, credentials: nil, region: MU.curRegion)
          found = {}
          resp = MU::Cloud::AWS.ec2(region: region, credentials: credentials).describe_network_interfaces(
            filters: [
              {
                name: "group-id",
                values: sg_ids
              }
            ]
          )
          return found if !resp or !resp.network_interfaces

          resp.network_interfaces.each { |iface|
# It's not impossible to reverse-map to the resource that owns this, but most
# of the time it'll be something we can't manage directly, so let's leave it be
#MU.log iface.network_interface_id+": #{iface.attachment.instance_owner_id} (#{iface.attachment.attach_time})", MU::NOTICE, details: iface.description
            iface.groups.each { |sg|
              found[sg.group_id] ||= {}
              found[sg.group_id][iface.network_interface_id] = iface
            }
          }

          found
        end
ingressRuleAddtlSchema() click to toggle source

Return an AWS-specific chunk of schema commonly used in the ingress_rules parameter of other resource types. @return [Hash]

# File modules/mu/providers/aws/firewall_rule.rb, line 522
def self.ingressRuleAddtlSchema
  {
    "items" => {
      "properties" => {
        "sgs" => {
          "type" => "array",
          "items" => {
            "description" => "Other AWS Security Groups; resources that are associated with this group will have this rule applied to their traffic",
            "type" => "string"
          }
        },
        "lbs" => {
          "type" => "array",
          "items" => {
            "description" => "AWS Load Balancers which will have this rule applied to their traffic",
            "type" => "string"
          }
        }
      }
    }
  }
end
isGlobal?() click to toggle source

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

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

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

Calls superclass method
# File modules/mu/providers/aws/firewall_rule.rb, line 28
def initialize(**args)
  super
  if !@vpc.nil?
    @mu_name ||= @deploy.getResourceName(@config['name'], need_unique_string: true)
  else
    @mu_name ||= @deploy.getResourceName(@config['name'])
  end

end
quality() click to toggle source

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

# File modules/mu/providers/aws/firewall_rule.rb, line 378
def self.quality
  MU::Cloud::RELEASE
end
rulesToBoK(ip_permissions, egress: false) click to toggle source

Given a set of AWS Security Group rules, convert them back to our language.

# File modules/mu/providers/aws/firewall_rule.rb, line 301
def self.rulesToBoK(ip_permissions, egress: false)
  rules = []

  ip_permissions.each { |r|
    rule = {}
    if r.from_port and r.to_port
      if r.from_port == r.to_port
        rule["port"] = r.from_port
      elsif !(r.from_port == 0 and r.to_port == 65535)
        rule["port_range"] = r.from_port.to_s+"-"+ r.to_port.to_s
      end
    end

    if r.ip_ranges and r.ip_ranges.size > 0
      rule["hosts"] = r.ip_ranges.map { |c| c.cidr_ip }
      if r.ip_ranges.first.description
        rule["comment"] = r.ip_ranges.first.description
      end
    end

    if r.ip_protocol =="-1"
      rule["proto"] = "all"
    else
      rule["proto"] = r.ip_protocol
    end

    if !r.user_id_group_pairs.empty?
      rule["firewall_rules"] = []
      # XXX how do rules referencing LBs look from here? for us that
      # really means references to a loadbalancer's primary SG
      r.user_id_group_pairs.each { |g|
        if g.group_id == @cloud_id
          bok['self_referencing'] = true
          next
        end

        rule['firewall_rules'] << MU::Config::Ref.get(
          cloud: "AWS",
          type: "firewall_rules",
          id: g.group_id,
          habitat: MU::Config::Ref.get(
            cloud: "AWS",
            type: "habitats",
            id: g.user_id,
          )
        )
        if g.vpc_peering_connection_id
          MU.log "Security Group #{self.to_s} has a rule referencing a peering connection (#{g.vpc_peering_connection_id}) and I don't know how to support that right now", MU::WARN
          next
        end
      }
    end

    rule.delete("comment") if rule["comment"] == "Added by Mu"

    rule['egress'] = true if egress

    # Don't bother with the default egress rule
    if egress and rule['hosts'] == ["0.0.0.0/0"] and rule["proto"] == "all"
      next
    end

    rules << rule
  }

  rules
end
schema(_config) click to toggle source

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

# File modules/mu/providers/aws/firewall_rule.rb, line 548
def self.schema(_config)
  toplevel_required = []
  schema = {
    "rules" => {
      "items" => {
        "properties" => {
          "firewall_rules" => {
            "type" => "array",
            "items" => MU::Config::FirewallRule.reference
          },
          "sgs" => {
            "type" => "array",
            "items" => {
              "description" => "DEPRECATED, use +firewall_rules+. Other AWS Security Groups; resources that are associated with this group will have this rule applied to their traffic",
              "type" => "string"
            }
          },
          "loadbalancers" => {
            "type" => "array",
            "items" => MU::Config::LoadBalancer.reference
          },
          "lbs" => {
            "type" => "array",
            "items" => {
              "description" => "DEPRECATED, use +loadbalancers+. AWS Load Balancers which will have this rule applied to their traffic",
              "type" => "string"
            }
          }
        }
      }
    }
  }
  [toplevel_required, schema]
end
validateConfig(acl, configurator) click to toggle source

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

# File modules/mu/providers/aws/firewall_rule.rb, line 587
def self.validateConfig(acl, configurator)
  ok = true
  if !acl["vpc_name"].nil? or !acl["vpc_id"].nil?
    acl['vpc'] = Hash.new
    if acl["vpc_id"].nil?
      acl['vpc']["vpc_id"] = config.getTail("vpc_id", value: acl["vpc_id"], prettyname: "Firewall Ruleset #{acl['name']} Target VPC",  cloudtype: "AWS::EC2::VPC::Id") if acl["vpc_id"].is_a?(String)
    elsif !acl["vpc_name"].nil?
      acl['vpc']['vpc_name'] = acl["vpc_name"]
    end
  end
  if !acl["vpc"].nil?
    # Drop meaningless subnet references
    acl['vpc'].delete("subnets")
    acl['vpc'].delete("subnet_id")
    acl['vpc'].delete("subnet_name")
    acl['vpc'].delete("subnet_pref")
  end
  acl['rules'] ||= {}
  acl['rules'].each { |rule|
    if !rule['sgs'].nil?
      rule['firewall_rules'] ||= []
      rule['sgs'].each { |sg_name|
              if configurator.haveLitterMate?(sg_name, "firewall_rules") and sg_name != acl['name']
          rule['firewall_rules'] << MU::Config::Ref.get(
            type: "firewall_rule",
            name: sg_name,
            cloud: "AWS",
            region: acl['region']
          )
        elsif sg_name == acl['name']
          acl['self_referencing'] = true
        else
          rule['firewall_rules'] << MU::Config::Ref.get(
            type: "firewall_rule",
            id: sg_name,
            cloud: "AWS",
            region: acl['region']
          )
        end
      }
    end
    rule.delete("sgs")

    if !rule['lbs'].nil?
      rule['loadbalancers'] ||= []
      rule['lbs'].each { |lb_name|
              if configurator.haveLitterMate?(lb_name, "loadbalancers")
          rule['loadbalancers'] << MU::Config::Ref.get(
            type: "loadbalancer",
            name: lb_name,
            cloud: "AWS",
            region: acl['region']
          )
        else
          rule['loadbalancers'] << MU::Config::Ref.get(
            type: "loadbalancer",
            id: lb_name,
            cloud: "AWS",
            region: acl['region']
          )
        end
      }
      rule.delete("lbs")
    end

    if rule['firewall_rules']
      rule['firewall_rules'].each { |sg|
        if sg['name'] and !sg['deploy_id']
          MU::Config.addDependency(acl, sg['name'], "firewall_rule", my_phase: "groom")
        end
      }
    end

    if rule['loadbalancers']
      rule['loadbalancers'].each { |lb|
        if lb['name'] and !lb['deploy_id']
          MU::Config.addDependency(acl, lb['name'], "loadbalancer", their_phase: "groom")
        end
      }
    end
  }

  acl['dependencies'].uniq!
  ok
end

Private Class Methods

revoke_rules(sg, egress: false, region: MU.myregion, credentials: nil) click to toggle source
# File modules/mu/providers/aws/firewall_rule.rb, line 468
def self.revoke_rules(sg, egress: false, region: MU.myregion, credentials: nil)
  holes = sg.send(egress ? :ip_permissions_egress : :ip_permissions)

  to_revoke = []

  holes.each { |hole|
    to_revoke << MU.structToHash(hole)
    to_revoke.each { |rule|
      if !rule[:user_id_group_pairs].nil? and rule[:user_id_group_pairs].size == 0
        rule.delete(:user_id_group_pairs)
      elsif !rule[:user_id_group_pairs].nil?
        rule[:user_id_group_pairs].each { |group_ref|
          group_ref = MU.structToHash(group_ref)
          group_ref.delete(:group_name) if group_ref[:group_id]
        }
      end

      if !rule[:ip_ranges].nil? and rule[:ip_ranges].size == 0
        rule.delete(:ip_ranges)
      end

      if !rule[:prefix_list_ids].nil? and rule[:prefix_list_ids].size == 0
        rule.delete(:prefix_list_ids)
      end

      if !rule[:ipv_6_ranges].nil? and rule[:ipv_6_ranges].size == 0
        rule.delete(:ipv_6_ranges)
      end
    }
  }

  if to_revoke.size > 0
    begin
      if egress
        MU::Cloud::AWS.ec2(credentials: credentials, region: region).revoke_security_group_egress(
          group_id: sg.group_id,
          ip_permissions: to_revoke
        )
      else
        MU::Cloud::AWS.ec2(credentials: credentials, region: region).revoke_security_group_ingress(
          group_id: sg.group_id,
          ip_permissions: to_revoke
        )
      end
    rescue Aws::EC2::Errors::InvalidPermissionNotFound
      MU.log "Rule in #{sg.group_id} disappeared before I could remove it", MU::WARN
    end
  end

end

Public Instance Methods

addRule(hosts, proto: "tcp", port: nil, egress: false, port_range: "0-65535", comment: nil) click to toggle source

Insert a rule into an existing security group.

@param hosts [Array<String>]: An array of CIDR network addresses to which this rule will apply. @param proto [String]: One of “tcp,” “udp,” or “icmp” @param port [Integer]: A port number. Only valid with udp or tcp. @param egress [Boolean]: Whether this is an egress ruleset, instead of ingress. @param port_range [String]: A port range descriptor (e.g. 0-65535). Only valid with udp or tcp. @return [void]

# File modules/mu/providers/aws/firewall_rule.rb, line 141
def addRule(hosts, proto: "tcp", port: nil, egress: false, port_range: "0-65535", comment: nil)
  rule = Hash.new
  rule["proto"] = proto
  sgs = []
  hosts = [hosts] if hosts.is_a?(String)
  hosts.each { |h|
    sgs << h if h.match(/^sg-/)
  }
  if sgs.size > 0
    rule["firewall_rules"] ||= []
    rule["firewall_rules"].concat(sgs.map { |s|
      MU::Config::Ref.get(
        id: s,
        region: @region,
        credentials: @credentials,
        cloud: "AWS",
        type: "firewall_rule",
        dummy_ok: true
      )
    })
  end
  hosts = hosts - sgs
  rule["hosts"] = hosts if hosts.size > 0

  if port != nil
    port = port.to_s if !port.is_a?(String)
    rule["port"] = port
  else
    rule["port_range"] = port_range
  end
  ec2_rule = convertToEc2([rule])

  begin
    if egress
      MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).authorize_security_group_egress(
        group_id: @cloud_id,
        ip_permissions: ec2_rule
      )
    else
      MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).authorize_security_group_ingress(
        group_id: @cloud_id,
        ip_permissions: ec2_rule
      )
    end
  rescue Aws::EC2::Errors::InvalidPermissionDuplicate
    MU.log "Attempt to add duplicate rule to #{@cloud_id}", MU::DEBUG, details: ec2_rule
    # Ensure that, at least, the description field gets updated on
    # existing rules
    if comment
      if egress
        MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).update_security_group_rule_descriptions_egress(
          group_id: @cloud_id,
          ip_permissions: ec2_rule
        )
      else
        MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).update_security_group_rule_descriptions_ingress(
          group_id: @cloud_id,
          ip_permissions: ec2_rule
        )
      end
    end
  end
end
arn() click to toggle source

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

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

Called by {MU::Deploy#createResources}

# File modules/mu/providers/aws/firewall_rule.rb, line 39
def create
  vpc_id = @vpc.cloud_id if !@vpc.nil?
  groupname = @mu_name
  description = groupname

  sg_struct = {
    :group_name => groupname,
    :description => description
  }
  if !vpc_id.nil?
    sg_struct[:vpc_id] = vpc_id
  end

  begin
    MU.log "Creating EC2 Security Group #{groupname}", details: sg_struct

    secgroup = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).create_security_group(sg_struct)
    @cloud_id = secgroup.group_id
  rescue Aws::EC2::Errors::InvalidGroupDuplicate
    MU.log "EC2 Security Group #{groupname} already exists, using it", MU::NOTICE
    filters = [{name: "group-name", values: [groupname]}]
    filters << {name: "vpc-id", values: [vpc_id]} if !vpc_id.nil?

    secgroup = MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_security_groups(filters: filters).security_groups.first
    if secgroup.nil?
      raise MuError, "Failed to locate security group named #{groupname}, even though EC2 says it already exists", caller
    end
    @cloud_id = secgroup.group_id
  end

  begin
    MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).describe_security_groups(group_ids: [secgroup.group_id])
  rescue Aws::EC2::Errors::InvalidGroupNotFound
    MU.log "#{secgroup.group_id} not yet ready, waiting...", MU::NOTICE
    sleep 10
    retry
  end

  MU::Cloud::AWS.createStandardTags(secgroup.group_id, region: @region, credentials: @credentials)
  MU::Cloud::AWS.createTag(secgroup.group_id, "Name", groupname, region: @region, credentials: @credentials)

  if @config['optional_tags']
    MU::MommaCat.listOptionalTags.each { |key, value|
      MU::Cloud::AWS.createTag(secgroup.group_id, key, value, region: @region, credentials: @credentials)
    }
  end

  if @config['tags']
    @config['tags'].each { |tag|
      MU::Cloud::AWS.createTag(secgroup.group_id, tag['key'], tag['value'], region: @region, credentials: @credentials)
    }
  end

  egress = false
  egress = true if !vpc_id.nil?
  # XXX the egress logic here is a crude hack, this really needs to be
  # done at config level
  setRules(
    [],
    add_to_self: @config['self_referencing'],
    ingress: true,
    egress: egress
  )

  MU.log "EC2 Security Group #{groupname} is #{secgroup.group_id}", MU::DEBUG
  return secgroup.group_id
end
groom() click to toggle source

Called by {MU::Deploy#createResources}

# File modules/mu/providers/aws/firewall_rule.rb, line 108
def groom
  if !@config['rules'].nil? and @config['rules'].size > 0
    egress = false
    egress = true if !@vpc.nil?
    # XXX the egress logic here is a crude hack, this really needs to be
    # done at config level
    setRules(
      @config['rules'],
      add_to_self: @config['self_referencing'],
      ingress: true,
      egress: egress
    )
  end
end
notify() click to toggle source

Log metadata about this ruleset to the currently running deployment

# File modules/mu/providers/aws/firewall_rule.rb, line 124
def notify
  sg_data = MU.structToHash(
      MU::Cloud::FirewallRule.find(cloud_id: @cloud_id, region: @region)
  )
  sg_data["group_id"] = @cloud_id
  sg_data["cloud_id"] = @cloud_id
  return sg_data
end
toKitten(**_args) click to toggle source

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

# File modules/mu/providers/aws/firewall_rule.rb, line 251
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

  # Ignore groups created/managed by AWS
  if cloud_desc.group_name == "default" or
     cloud_desc.group_name.match(/^AWS-OpsWorks-/)
    return nil
  end

  # XXX identify if we'd be created by the ingress_rules of another
  # resource

  bok["name"] = cloud_desc.group_name

  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.tags and !cloud_desc.tags.empty?
    bok['tags'] = MU.structToHash(cloud_desc.tags, stringify_keys: true)
    realname = MU::Adoption.tagsToName(bok['tags'])
    bok['name'] = realname if realname
  end

  if cloud_desc.ip_permissions
    bok["rules"] ||= []
    bok["rules"].concat(MU::Cloud::AWS::FirewallRule.rulesToBoK(cloud_desc.ip_permissions))
    bok["rules"].concat(MU::Cloud::AWS::FirewallRule.rulesToBoK(cloud_desc.ip_permissions_egress, egress: true))
  end

  bok
end

Private Instance Methods

convertToEc2(rules) click to toggle source

Convert our config languages description of firewall rules into Amazon's. Our rule structure is as defined in MU::Config.

# File modules/mu/providers/aws/firewall_rule.rb, line 845
def convertToEc2(rules)
  ec2_rules = []
  if rules != nil
    rules.uniq!

    rules.each { |rule|
      ec2_rule = {}
      rule["comment"] ||= "Added by Mu"


      rule['proto'] ||= "tcp"
      ec2_rule[:ip_protocol] = rule['proto']

      p_start = nil
      p_end = nil
      if rule['port_range']
        p_start, p_end = rule['port_range'].to_s.split(/\s*-\s*/)
      elsif rule['port']
        p_start = rule['port'].to_i
        p_end = rule['port'].to_i
      elsif rule['proto'] != "icmp"
        MU.log "Can't create a TCP or UDP security group rule without specifying ports, assuming 'all'", MU::WARN, details: rule
        p_start = "0"
        p_end = "65535"
      end

      if rule['proto'] != "icmp"
        if p_start.nil? or p_end.nil?
          raise MuError, "Got nil ports out of rule #{rule}"
        end
        ec2_rule[:from_port] = p_start.to_i
        ec2_rule[:to_port] = p_end.to_i
      else
        ec2_rule[:from_port] = -1
        ec2_rule[:to_port] = -1
      end

      if (!defined? rule['hosts'] or !rule['hosts'].is_a?(Array)) and
         (!defined? rule['firewall_rules'] or !rule['firewall_rules'].is_a?(Array)) and
         (!defined? rule['loadbalancers'] or !rule['loadbalancers'].is_a?(Array))
        rule['hosts'] = ["0.0.0.0/0"]
      end
      ec2_rule[:ip_ranges] = []
      ec2_rule[:user_id_group_pairs] = []

      if !rule['hosts'].nil?
        rule['hosts'].uniq!
        rule['hosts'].each { |cidr|
          next if cidr.nil? # XXX where is that coming from?
          cidr = cidr + "/32" if cidr.match(/^\d+\.\d+\.\d+\.\d+$/)
          ec2_rule[:ip_ranges] << {cidr_ip: cidr, description: rule['comment']}
        }
      end

      if !rule['loadbalancers'].nil?
        rule['loadbalancers'].uniq!
        rule['loadbalancers'].each { |lb|
          lb_ref = MU::Config::Ref.get(lb)

          if !lb_ref.kitten or !lb_ref.kitten.cloud_desc
            MU.log "Security Group #{@mu_name} failed to get cloud descriptor for referenced load balancer", MU::ERR, details: lb_ref
            next
          end

          lb_ref.kitten.cloud_desc.security_groups.each { |lb_sg|
            # XXX this probably has to infer things like region,
            # credentials, etc from the load balancer ref
            lb_sg_desc = MU::Cloud::AWS::FirewallRule.find(cloud_id: lb_sg)
            owner_id = if lb_sg_desc and lb_sg_desc.size == 1
              lb_sg_desc.values.first.owner_id
            else
              MU::Cloud::AWS.credToAcct(@credentials)
            end
            ec2_rule[:user_id_group_pairs] << {
              user_id: owner_id,
              group_id: lb_sg,
              description: rule['comment']
            }
          }
        }
      end

      if !rule['firewall_rules'].nil?
        rule['firewall_rules'].uniq!
        rule['firewall_rules'].each { |sg|
          sg_ref = MU::Config::Ref.get(sg)

          if !sg_ref.kitten or !sg_ref.kitten.cloud_desc
            MU.log "Security Group #{@mu_name} failed to get cloud descriptor for referenced Security Group", MU::ERR, details: sg_ref
            next
          end

          ec2_rule[:user_id_group_pairs] << {
            user_id: sg_ref.kitten.cloud_desc.owner_id,
            group_id: sg_ref.cloud_id,
            description: rule['comment']
          }

        }
      end

      ec2_rule[:user_id_group_pairs].uniq!
      ec2_rule[:ip_ranges].uniq!
      ec2_rule.delete(:ip_ranges) if ec2_rule[:ip_ranges].empty?
      ec2_rule.delete(:user_id_group_pairs) if ec2_rule[:user_id_group_pairs].empty?

      # if !ec2_rule[:user_id_group_pairs].nil? and
        # ec2_rule[:user_id_group_pairs].size > 0 and
          # !ec2_rule[:ip_ranges].nil? and
          # ec2_rule[:ip_ranges].size > 0
        # MU.log "Cannot specify ip_ranges and user_id_group_pairs", MU::ERR
        # raise MuError, "Cannot specify ip_ranges and user_id_group_pairs"
      # end

      # if !ec2_rule[:user_id_group_pairs].nil? and
          # ec2_rule[:user_id_group_pairs].size > 0
        # ec2_rule.delete(:ip_ranges)
        # ec2_rule[:user_id_group_pairs].uniq!
      # elsif !ec2_rule[:ip_ranges].nil? and
          # ec2_rule[:ip_ranges].size > 0
        # ec2_rule.delete(:user_id_group_pairs)
        # ec2_rule[:ip_ranges].uniq!
      # end
      ec2_rules << ec2_rule
    }
  end

  ec2_rules.uniq
end
purge_extraneous_rules(ec2_rules, ext_permissions) click to toggle source
# File modules/mu/providers/aws/firewall_rule.rb, line 705
def purge_extraneous_rules(ec2_rules, ext_permissions)
  # Purge any old rules that we're sure we created (check the comment)
  # but which are no longer configured.
  ext_permissions.each { |ext_rule|
    haverule = false
    ec2_rules.each { |rule|
      if rule[:from_port] == ext_rule[:from_port] and
         rule[:to_port] == ext_rule[:to_port] and
         rule[:ip_protocol] == ext_rule[:ip_protocol]
        haverule = true
        break
      end
    }
    next if haverule

    mu_comments = false
    (ext_rule[:user_id_group_pairs] + ext_rule[:ip_ranges]).each { |entry|
      if entry[:description] == "Added by Mu"
        mu_comments = true
      else
        mu_comments = false
        break
      end
    }

    if mu_comments
      ext_rule.keys.each { |k|
        if ext_rule[k].nil? or ext_rule[k] == []
          ext_rule.delete(k)
        end
      }
      MU.log "Removing unconfigured rule in #{@mu_name}", MU::WARN, details: ext_rule
      MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).revoke_security_group_ingress(
        group_id: @cloud_id,
        ip_permissions: [ext_rule]
      )
    end
  }
end
setRules(rules, add_to_self: false, ingress: true, egress: false) click to toggle source

Manufacture an EC2 security group. The second parameter, rules, is an “ingress_rules” structure parsed and validated by MU::Config.

# File modules/mu/providers/aws/firewall_rule.rb, line 749
def setRules(rules, add_to_self: false, ingress: true, egress: false)
  # XXX warn about attempt to set rules before we exist
  return if rules.nil? or rules.size == 0 or !@cloud_id

  # add_to_self means that this security is a "member" of its own rules
  # (which is to say, objects that have this SG are allowed in my these
  # rules)
  if add_to_self
    rules.each { |rule|
      if rule['sgs'].nil? or !rule['sgs'].include?(@cloud_id)
        new_rule = rule.clone
        new_rule.delete('hosts')
        rule['sgs'] = Array.new if rule['sgs'].nil?
        rule['sgs'] << @cloud_id
      end
    }
  end

  ec2_rules = convertToEc2(rules)
  return if ec2_rules.nil?

  ext_permissions = MU.structToHash(cloud_desc(use_cache: false).ip_permissions)

  purge_extraneous_rules(ec2_rules, ext_permissions)

  ec2_rules.uniq!
  ec2_rules.each { |rule|
    haverule = nil
    different = false
    ext_permissions.each { |ext_rule|
      if rule[:from_port] == ext_rule[:from_port] and
         rule[:to_port] == ext_rule[:to_port] and
         rule[:ip_protocol] == ext_rule[:ip_protocol]
        haverule = ext_rule
        ext_rule.keys.each { |k|
          if ext_rule[k].nil? or ext_rule[k] == []
            haverule.delete(k)
          end
          different = true if rule[k] != ext_rule[k]
        }
        break
      end
    }
    if haverule and !different
      MU.log "Security Group rule already up-to-date in #{@mu_name}", MU::DEBUG, details: rule
      next
    end

    MU.log "Setting #{ingress ? "ingress" : "egress"} rule in Security Group #{@mu_name} (#{@cloud_id})", MU::NOTICE, details: rule

    MU.retrier([Aws::EC2::Errors::InvalidGroupNotFound], max: 10, wait: 10, ignoreme: [Aws::EC2::Errors::InvalidPermissionDuplicate]) {
      if ingress
        if haverule
          begin
            MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).revoke_security_group_ingress(
              group_id: @cloud_id,
              ip_permissions: [haverule]
            )
          rescue Aws::EC2::Errors::InvalidPermissionNotFound
          end
        end
        begin
          MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).authorize_security_group_ingress(
            group_id: @cloud_id,
            ip_permissions: [rule]
          )
        rescue Aws::EC2::Errors::InvalidParameterCombination => e
          MU.log "FirewallRule #{@mu_name} had a bogus rule: #{e.message}", MU::ERR, details: rule
          raise e
        end
      end

      if egress
        if haverule
          begin
            MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).revoke_security_group_egress(
              group_id: @cloud_id,
              ip_permissions: [haverule]
            )
          rescue Aws::EC2::Errors::InvalidPermissionNotFound
          end
        end
        MU::Cloud::AWS.ec2(region: @region, credentials: @credentials).authorize_security_group_egress(
          group_id: @cloud_id,
          ip_permissions: [rule]
        )
      end
    }
  }

end