# 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 firewall ruleset as configured in {MU::Config::BasketofKittens::firewall_rules} class FirewallRule < MU::Cloud::FirewallRule require "mu/providers/aws/vpc" @admin_sgs = Hash.new @admin_sg_semaphore = Mutex.new # 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 if !@vpc.nil? @mu_name ||= @deploy.getResourceName(@config['name'], need_unique_string: true) else @mu_name ||= @deploy.getResourceName(@config['name']) end end # Called by {MU::Deploy#createResources} 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: @config['region'], credentials: @config['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: @config['region'], credentials: @config['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: @config['region'], credentials: @config['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: @config['region'], credentials: @config['credentials']) MU::Cloud::AWS.createTag(secgroup.group_id, "Name", groupname, region: @config['region'], credentials: @config['credentials']) if @config['optional_tags'] MU::MommaCat.listOptionalTags.each { |key, value| MU::Cloud::AWS.createTag(secgroup.group_id, key, value, region: @config['region'], credentials: @config['credentials']) } end if @config['tags'] @config['tags'].each { |tag| MU::Cloud::AWS.createTag(secgroup.group_id, tag['key'], tag['value'], region: @config['region'], credentials: @config['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 # Called by {MU::Deploy#createResources} 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 # Log metadata about this ruleset to the currently running deployment def notify sg_data = MU.structToHash( MU::Cloud::FirewallRule.find(cloud_id: @cloud_id, region: @config['region']) ) sg_data["group_id"] = @cloud_id sg_data["cloud_id"] = @cloud_id return sg_data end # Insert a rule into an existing security group. # # @param hosts [Array]: 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] 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, cloud: "AWS", type: "firewall_rule" ) }) 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: @config['region'], credentials: @config['credentials']).authorize_security_group_egress( group_id: @cloud_id, ip_permissions: ec2_rule ) else MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['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: @config['region'], credentials: @config['credentials']).update_security_group_rule_descriptions_egress( group_id: @cloud_id, ip_permissions: ec2_rule ) else MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).update_security_group_rule_descriptions_ingress( group_id: @cloud_id, ip_permissions: ec2_rule ) end end end end # Canonical Amazon Resource Number for this resource # @return [String] def arn "arn:"+(MU::Cloud::AWS.isGovCloud?(@config["region"]) ? "aws-us-gov" : "aws")+":ec2:"+@config['region']+":"+MU::Cloud::AWS.credToAcct(@config['credentials'])+":security-group/"+@cloud_id end # Locate an existing security group or groups and return an array containing matching AWS resource descriptors for those that match. # @return [Array>]: The cloud provider's complete descriptions of matching FirewallRules 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 # 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" => @config['credentials'], "cloud_id" => @cloud_id, "region" => @config['region'] } if !cloud_desc MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config return nil end # 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 # Given a set of AWS Security Group rules, convert them back to our # language. 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 # 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 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] 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 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 private_class_method :revoke_rules # Return an AWS-specific chunk of schema commonly used in the +ingress_rules+ parameter of other resource types. # @return [Hash] 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 # Cloud-specific configuration properties. # @param _config [MU::Config]: The calling MU::Config object # @return [Array]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource 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 # 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 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", no_create_wait: true) end } end if rule['loadbalancers'] rule['loadbalancers'].each { |lb| if lb['name'] and !lb['deploy_id'] MU::Config.addDependency(acl, lb['name'], "loadbalancer", phase: "groom") end } end } acl['dependencies'].uniq! ok end # Look up all the network interfaces using one or more security groups # @param sg_ids [Array] # @param credentials [String] # @param region [String] # @return [Hash] 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 private 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: @config['region'], credentials: @config['credentials']).revoke_security_group_ingress( group_id: @cloud_id, ip_permissions: [ext_rule] ) end } end ######################################################################### # Manufacture an EC2 security group. The second parameter, rules, is an # "ingress_rules" structure parsed and validated by MU::Config. ######################################################################### 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: @config['region'], credentials: @config['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: @config['region'], credentials: @config['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: @config['region'], credentials: @config['credentials']).revoke_security_group_egress( group_id: @cloud_id, ip_permissions: [haverule] ) rescue Aws::EC2::Errors::InvalidPermissionNotFound end end MU::Cloud::AWS.ec2(region: @config['region'], credentials: @config['credentials']).authorize_security_group_egress( group_id: @cloud_id, ip_permissions: [rule] ) end } } end ####################################################################### # Convert our config languages description of firewall rules into # Amazon's. Our rule structure is as defined in MU::Config. ####################################################################### 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 end #class end #class end end #module