class MU::Cloud::Azure::FirewallRule

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

Attributes

rulesets[R]

Public Class Methods

cleanup(**args) click to toggle source

Stub method. Azure resources are cleaned up by removing the parent resource group. @return [void]

# File modules/mu/providers/azure/firewall_rule.rb, line 333
def self.cleanup(**args)
end
find(**args) click to toggle source

Locate and return cloud provider descriptors of this resource type which match the provided parameters, or all visible resources if no filters are specified. At minimum, implementations of find must honor credentials and cloud_id arguments. We may optionally support other search methods, such as tag_key and tag_value, or cloud-specific arguments like project. See also {MU::MommaCat.findStray}. @param args [Hash]: Hash of named arguments passed via Ruby's double-splat @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching resources

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

  # Azure resources are namedspaced by resource group. If we weren't
  # told one, we may have to search all the ones we can see.
  resource_groups = if args[:resource_group]
    [args[:resource_group]]
  elsif args[:cloud_id] and args[:cloud_id].is_a?(MU::Cloud::Azure::Id)
    [args[:cloud_id].resource_group]
  else
    MU::Cloud::Azure.resources(credentials: args[:credentials]).resource_groups.list.map { |rg| rg.name }
  end

  if args[:cloud_id]
    id_str = args[:cloud_id].is_a?(MU::Cloud::Azure::Id) ? args[:cloud_id].name : args[:cloud_id]
    resource_groups.each { |rg|
      begin
        resp = MU::Cloud::Azure.network(credentials: args[:credentials]).network_security_groups.get(rg, id_str)
        next if resp.nil?
        found[Id.new(resp.id)] = resp
      rescue MU::Cloud::Azure::APIError
        # this is fine, we're doing a blind search after all
      end
    }
  else
    if args[:resource_group]
      MU::Cloud::Azure.network(credentials: args[:credentials]).network_security_groups.list(args[:resource_group]).each { |net|
        found[Id.new(net.id)] = net
      }
    else
      MU::Cloud::Azure.network(credentials: args[:credentials]).network_security_groups.list_all.each { |net|
        found[Id.new(net.id)] = net
      }
    end
  end

  found
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/azure/firewall_rule.rb, line 320
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/azure/firewall_rule.rb, line 27
def initialize(**args)
  super

  if !mu_name.nil?
    @mu_name = mu_name
  else
    @mu_name = @deploy.getResourceName(@config['name'], max_length: 61)
  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/azure/firewall_rule.rb, line 326
def self.quality
  MU::Cloud::BETA
end
schema(_config = nil) 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/azure/firewall_rule.rb, line 355
        def self.schema(_config = nil)
          toplevel_required = []
          hosts_schema = MU::Config::CIDR_PRIMITIVE
          hosts_schema["pattern"] = "^(\\d+\\.\\d+\\.\\d+\\.\\d+\/[0-9]{1,2}|\\*)$"
          schema = {
            "rules" => {
              "items" => {
                "properties" => {
                  "hosts" => {
                    "type" => "array",
                    "items" => hosts_schema
                  },
                  "weight" => {
                    "type" => "integer",
                    "description" => "Explicitly set a priority for this firewall rule, between 100 and 2096, with lower numbered priority rules having greater precedence."
                  },
                  "deny" => {
                    "type" => "boolean",
                    "default" => false,
                    "description" => "Set this rule to +DENY+ traffic instead of +ALLOW+"
                  },
                  "proto" => {
                    "description" => "The protocol to allow with this rule. The +standard+ keyword will expand to a series of identical rules covering +tcp+ and +udp; the +all+ keyword will allow all supported protocols. Currently only +tcp+ and +udp+ are supported by Azure, so the end result of these two keywords is identical.",
                    "enum" => ["all", "standard", "tcp", "udp"],
                    "default" => "standard"
                  },
#                  "source_tags" => {
#                    "type" => "array",
#                    "description" => "VMs with these tags, from which traffic will be allowed",
#                    "items" => {
#                      "type" => "string"
#                    }
#                  },
#                  "source_service_accounts" => {
#                    "type" => "array",
#                    "description" => "Resources using these service accounts, from which traffic will be allowed",
#                    "items" => {
#                      "type" => "string"
#                    }
#                  },
#                  "target_tags" => {
#                    "type" => "array",
#                    "description" => "VMs with these tags, to which traffic will be allowed",
#                    "items" => {
#                      "type" => "string"
#                    }
#                  },
#                  "target_service_accounts" => {
#                    "type" => "array",
#                    "description" => "Resources using these service accounts, to which traffic will be allowed",
#                    "items" => {
#                      "type" => "string"
#                    }
#                  }
                }
              }
            },
          }
          [toplevel_required, schema]
        end
validateConfig(acl, config) 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 config [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/azure/firewall_rule.rb, line 420
def self.validateConfig(acl, config)
  ok = true
  acl['region'] ||= MU::Cloud::Azure.myRegion(acl['credentials'])

  append = []
  delete = []
  acl['rules'] ||= []
  acl['rules'].concat(config.adminFirewallRuleset(cloud: "Azure", region: acl['region'], rules_only: true))
  acl['rules'].each { |r|
    if r["weight"] and (r["weight"] < 100 or r["weight"] > 4096)
      MU.log "FirewallRule #{acl['name']} weight must be between 100 and 4096", MU::ERR
      ok = false
    end
    if r["hosts"]
      r["hosts"].each { |cidr|
        r["hosts"] << "*" if cidr == "0.0.0.0/0"
      }
      r["hosts"].delete("0.0.0.0/0")
    end

    if (!r['hosts'] or r['hosts'].empty?) and
       (!r['lbs'] or r['lbs'].empty?) and
       (!r['sgs'] or r['sgs'].empty?)
      r["hosts"] = ["*"]
      MU.log "FirewallRule #{acl['name']} did not specify any hosts, sgs or lbs, defaulting this rule to allow 0.0.0.0/0", MU::NOTICE
    end


    if r['proto'] == "standard"
      ["tcp", "udp"].each { |p|
        newrule = r.dup
        newrule['proto'] = p
        append << newrule
      }
      delete << r
    elsif r['proto'] == "all" or !r['proto']
      r['proto'] = "asterisk" # legit, the name of the constant
    end
  }
  delete.each { |r|
    acl['rules'].delete(r)
  }
  acl['rules'].concat(append)

  ok
end

Public Instance Methods

addRule(hosts, proto: "tcp", port: nil, egress: false, port_range: "0-65535", sgs: [], lbs: [], deny: false, weight: nil, oldrules: nil, num: 0, description: "") 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 [Array<Boolean,OpenStruct>]

# File modules/mu/providers/azure/firewall_rule.rb, line 116
        def addRule(hosts, proto: "tcp", port: nil, egress: false, port_range: "0-65535", sgs: [], lbs: [], deny: false, weight: nil, oldrules: nil, num: 0, description: "")
          if !oldrules
            oldrules = {}
            cloud_desc(use_cache: false).security_rules.each { |rule|
              if rule.description and rule.description.match(/^#{Regexp.quote(@mu_name)} \d+:/)
                oldrules[rule.name] = rule
              end
            }
          end
          used_priorities = oldrules.values.map { |r| r.priority }

          rule_obj = MU::Cloud::Azure.network(:SecurityRule).new
          resolved_sgs = []
# XXX these are *Application* Security Groups, which are a different kind of
# artifact. They take no parameters. Are they essentially a stub that can be
# attached to certain artifacts to allow them to be referenced here?
# http://54.175.86.194/docs/azure/Azure/Network/Mgmt/V2019_02_01/ApplicationSecurityGroups.html#create_or_update-instance_method
          if sgs
            sgs.each { |sg|
# look up cloud id for... whatever these are
            }
          end

#          resolved_lbs = []
#          if lbs
#            lbs.each { |lb|
# TODO awaiting LoadBalancer implementation
#            }
#          end

          if egress
            rule_obj.direction = MU::Cloud::Azure.network(:SecurityRuleDirection)::Outbound
            if hosts and !hosts.empty?
              rule_obj.source_address_prefix = "*"
              if hosts == ["*"]
                rule_obj.destination_address_prefix = "*"
              else
                rule_obj.destination_address_prefixes = hosts
              end
            end
            if !resolved_sgs.empty?
              rule_obj.destination_application_security_groups = resolved_sgs
            end
            if !rule_obj.destination_application_security_groups and
               !rule_obj.destination_address_prefix and
               !rule_obj.destination_address_prefixes
              rule_obj.source_address_prefix = "*"
              rule_obj.destination_address_prefix = "*"
            end
          else
            rule_obj.direction = MU::Cloud::Azure.network(:SecurityRuleDirection)::Inbound
            if hosts and !hosts.empty?
              if hosts == ["*"]
                rule_obj.source_address_prefix = "*"
              else
                rule_obj.source_address_prefixes = hosts
              end
              rule_obj.destination_address_prefix = "*"
            end
            if !resolved_sgs.empty?
              rule_obj.source_application_security_groups = resolved_sgs
            end
            if !rule_obj.source_application_security_groups and
               !rule_obj.source_address_prefix and
               !rule_obj.source_address_prefixes
              # should probably only do this if a port or port_range is named
              rule_obj.source_address_prefix = "*"
              rule_obj.destination_address_prefix = "*"
            end
          end

          rname_port = "port-"
          if port and port.to_s != "-1"
            rule_obj.destination_port_range = port.to_s
            rname_port += port.to_s
          elsif port_range and port_range != "-1"
            rule_obj.destination_port_range = port_range
            rname_port += port_range
          else
            rule_obj.destination_port_range = "*"
            rname_port += "all"
          end

          # We don't bother supporting restrictions on originating ports,
          # because practically nobody does that.
          rule_obj.source_port_range = "*"

          rule_obj.protocol = MU::Cloud::Azure.network(:SecurityRuleProtocol).const_get(proto.capitalize)
          rname_proto = "proto-"+ (proto == "asterisk" ? "all" : proto)

          if deny
            rule_obj.access = MU::Cloud::Azure.network(:SecurityRuleAccess)::Deny
          else
            rule_obj.access = MU::Cloud::Azure.network(:SecurityRuleAccess)::Allow
          end

          rname = rule_obj.access.downcase+"-"+rule_obj.direction.downcase+"-"+rname_proto+"-"+rname_port+"-"+num.to_s

          if weight
            rule_obj.priority = weight
          elsif oldrules[rname]
            rule_obj.priority = oldrules[rname].priority
          else
            default_priority = 999
            begin
              default_priority += 1 + num
              rule_obj.priority = default_priority
            end while used_priorities.include?(default_priority)
          end
          used_priorities << rule_obj.priority

          rule_obj.description = "#{@mu_name} #{num.to_s}: #{rname}"
       
          # Now compare this to existing rules, and see if we need to update
          # anything.
          need_update = false
          if oldrules[rname]
            rule_obj.instance_variables.each { |var|
              oldval = oldrules[rname].instance_variable_get(var)
              newval = rule_obj.instance_variable_get(var)
              need_update = true if oldval != newval
            }

            [:@destination_address_prefix, :@destination_address_prefixes,
             :@destination_application_security_groups,
             :@destination_address_prefix,
             :@destination_address_prefixes,
             :@destination_application_security_groups].each { |var|
              next if !oldrules[rname].instance_variables.include?(var)
              oldval = oldrules[rname].instance_variable_get(var)
              newval = rule_obj.instance_variable_get(var)
              if newval.nil? and !oldval.nil? and !oldval.empty?
                need_update = true
              end
            }
          else
            need_update = true
          end

          if need_update
            if oldrules[rname]
              MU.log "Updating rule #{rname} in #{@mu_name}", MU::NOTICE, details: rule_obj
            else
              MU.log "Creating rule #{rname} in #{@mu_name}", details: rule_obj
            end

            resp = MU::Cloud::Azure.network(credentials: @config['credentials']).security_rules.create_or_update(@resource_group, @mu_name, rname, rule_obj)
            return [!oldrules[rname].nil?, resp]
          else
            return [false, oldrules[rname]]
          end

        end
create() click to toggle source

Called by {MU::Deploy#createResources}

# File modules/mu/providers/azure/firewall_rule.rb, line 41
def create
  create_update
end
groom() click to toggle source

Called by {MU::Deploy#createResources}

# File modules/mu/providers/azure/firewall_rule.rb, line 46
        def groom
          create_update

          oldrules = {}
          newrules = {}
          cloud_desc.security_rules.each { |rule|
            if rule.description and rule.description.match(/^#{Regexp.quote(@mu_name)} \d+:/)
              oldrules[rule.name] = rule
            end
          }
#          used_priorities = oldrules.values.map { |r| r.priority }

          newrules_semaphore = Mutex.new
          num_rules = 0

          rulethreads = []
          return if !@config['rules']
          @config['rules'].each { |rule_cfg|
            num_rules += 1
            rulethreads << Thread.new(rule_cfg, num_rules) { |rule, num|
              was_new, desc = addRule(
                rule["hosts"],
                proto: rule["proto"],
                port: rule["port"],
                egress: rule["egress"],
                port_range: rule["port_range"],
                sgs: rule["sgs"],
                lbs: rule["lbs"],
                deny: rule["deny"],
                weight: rule["weight"],
                oldrules: oldrules,
                num: num
              )

              newrules_semaphore.synchronize {
                newrules[desc.name] = desc
                if !was_new
                  oldrules[desc.name] = desc
                end
              }

            } # rulethreads
          }

          rulethreads.each { |t|
            t.join
          }

          # Purge old rules that we own (according to the description) but
          # which are not part of our current configuration.
          (oldrules.keys - newrules.keys).each { |oldrule|
            MU.log "Dropping unused rule #{oldrule} from #{@mu_name}", MU::NOTICE
            MU::Cloud::Azure.network(credentials: @config['credentials']).security_rules.delete(@resource_group, @mu_name, oldrule)
          }

        end
notify() click to toggle source

Log metadata about this ruleset to the currently running deployment

# File modules/mu/providers/azure/firewall_rule.rb, line 104
def notify
  MU.structToHash(cloud_desc)
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/azure/firewall_rule.rb, line 339
def toKitten(**args)

  bok = {
    "cloud" => "Azure",
    "name" => cloud_desc.name,
    "project" => @config['project'],
    "credentials" => @config['credentials'],
    "cloud_id" => @cloud_id.to_s
  }

  bok
end

Private Instance Methods

create_update() click to toggle source
# File modules/mu/providers/azure/firewall_rule.rb, line 469
def create_update

  fw_obj = MU::Cloud::Azure.network(:NetworkSecurityGroup).new
  fw_obj.location = @config['region']
  fw_obj.tags = @tags

  need_apply = false
  ext_ruleset = MU::Cloud::Azure.network(credentials: @config['credentials']).network_security_groups.get(
    @resource_group,
    @mu_name
  )
  if ext_ruleset
    @cloud_id = MU::Cloud::Azure::Id.new(ext_ruleset.id)
  end

  if !ext_ruleset
    MU.log "Creating Network Security Group #{@mu_name} in #{@config['region']}", details: fw_obj
    need_apply = true
  elsif ext_ruleset.location != fw_obj.location or
        ext_ruleset.tags != fw_obj.tags
    MU.log "Updating Network Security Group #{@mu_name} in #{@config['region']}", MU::NOTICE, details: fw_obj
    need_apply = true
  end

  if need_apply
    resp = MU::Cloud::Azure.network(credentials: @config['credentials']).network_security_groups.create_or_update(
      @resource_group,
      @mu_name,
      fw_obj
    )

    @cloud_id = MU::Cloud::Azure::Id.new(resp.id)
  end
end