text: # Copyright 2015 Red Hat, Inc.
  # All Rights Reserved.
  #
  #    Licensed under the Apache License, Version 2.0 (the "License"); you may
  #    not use this file except in compliance with the License. You may obtain
  #    a copy of the License at
  #
  #         http://www.apache.org/licenses/LICENSE-2.0
  #
  #    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.
  
  import collections
  
  import netaddr
  from neutron_lib import constants as n_consts
  
  from neutron.agent.linux.openvswitch_firewall import constants as ovsfw_consts
  from neutron.common import utils
  from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants \
          as ovs_consts
  
  CT_STATES = [
      ovsfw_consts.OF_STATE_ESTABLISHED_NOT_REPLY,
      ovsfw_consts.OF_STATE_NEW_NOT_ESTABLISHED]
  
  FLOW_FIELD_FOR_IPVER_AND_DIRECTION = {
      (n_consts.IP_VERSION_4, n_consts.EGRESS_DIRECTION): 'nw_dst',
      (n_consts.IP_VERSION_6, n_consts.EGRESS_DIRECTION): 'ipv6_dst',
      (n_consts.IP_VERSION_4, n_consts.INGRESS_DIRECTION): 'nw_src',
      (n_consts.IP_VERSION_6, n_consts.INGRESS_DIRECTION): 'ipv6_src',
  }
  
  FORBIDDEN_PREFIXES = (n_consts.IPv4_ANY, n_consts.IPv6_ANY)
  
  
  def is_valid_prefix(ip_prefix):
      # IPv6 have multiple ways how to describe ::/0 network, converting to
      # IPNetwork and back to string unifies it
      return (ip_prefix and
              str(netaddr.IPNetwork(ip_prefix)) not in FORBIDDEN_PREFIXES)
  
  
  def _assert_mergeable_rules(rule_conj_list):
      """Assert a given (rule, conj_ids) list has mergeable rules.
  
      The given rules must be the same except for port_range_{min,max}
      differences.
      """
      rule_tmpl = rule_conj_list[0][0].copy()
      rule_tmpl.pop('port_range_min', None)
      rule_tmpl.pop('port_range_max', None)
      for rule, conj_id in rule_conj_list[1:]:
          rule1 = rule.copy()
          rule1.pop('port_range_min', None)
          rule1.pop('port_range_max', None)
          if rule_tmpl != rule1:
              raise RuntimeError(
                  "Incompatible SG rules detected: %(rule1)s and %(rule2)s. "
                  "They cannot be merged. This should not happen." %
                  {'rule1': rule_tmpl, 'rule2': rule})
  
  
  def merge_common_rules(rule_conj_list):
      """Take a list of (rule, conj_id) and merge elements with the same rules.
      Return a list of (rule, conj_id_list).
      """
      if len(rule_conj_list) == 1:
          rule, conj_id = rule_conj_list[0]
          return [(rule, [conj_id])]
  
      _assert_mergeable_rules(rule_conj_list)
      rule_conj_map = collections.defaultdict(list)
      for rule, conj_id in rule_conj_list:
          rule_conj_map[(rule.get('port_range_min'),
                         rule.get('port_range_max'))].append(conj_id)
  
      result = []
      rule_tmpl = rule_conj_list[0][0]
      rule_tmpl.pop('port_range_min', None)
      rule_tmpl.pop('port_range_max', None)
      for (port_min, port_max), conj_ids in rule_conj_map.items():
          rule = rule_tmpl.copy()
          if port_min is not None:
              rule['port_range_min'] = port_min
          if port_max is not None:
              rule['port_range_max'] = port_max
          result.append((rule, conj_ids))
      return result
  
  
  def _merge_port_ranges_helper(port_range_item):
      # Sort with 'port' but 'min' things must come first.
      port, m, dummy = port_range_item
      return port * 2 + (0 if m == 'min' else 1)
  
  
  def merge_port_ranges(rule_conj_list):
      """Take a list of (rule, conj_id) and transform into a list
      whose rules don't overlap. Return a list of (rule, conj_id_list).
      """
      if len(rule_conj_list) == 1:
          rule, conj_id = rule_conj_list[0]
          return [(rule, [conj_id])]
  
      _assert_mergeable_rules(rule_conj_list)
      port_ranges = []
      for rule, conj_id in rule_conj_list:
          port_ranges.append((rule.get('port_range_min', 1), 'min', conj_id))
          port_ranges.append((rule.get('port_range_max', 65535), 'max', conj_id))
  
      port_ranges.sort(key=_merge_port_ranges_helper)
  
      # The idea here is to scan the port_ranges list in an ascending order,
      # keeping active conjunction IDs and range in cur_conj and cur_range_min.
      # A 'min' port_ranges item means an addition to cur_conj, while a 'max'
      # item means a removal.
      result = []
      rule_tmpl = rule_conj_list[0][0]
      cur_conj = set()
      cur_range_min = None
      for port, m, conj_id in port_ranges:
          if m == 'min':
              if cur_conj and cur_range_min != port:
                  rule = rule_tmpl.copy()
                  rule['port_range_min'] = cur_range_min
                  rule['port_range_max'] = port - 1
                  result.append((rule, list(cur_conj)))
              cur_range_min = port
              cur_conj.add(conj_id)
          else:
              if cur_range_min <= port:
                  rule = rule_tmpl.copy()
                  rule['port_range_min'] = cur_range_min
                  rule['port_range_max'] = port
                  result.append((rule, list(cur_conj)))
                  # The next port range without 'port' starts from (port + 1)
                  cur_range_min = port + 1
              cur_conj.remove(conj_id)
  
      if (len(result) == 1 and result[0][0]['port_range_min'] == 1 and
              result[0][0]['port_range_max'] == 65535):
          del result[0][0]['port_range_min']
          del result[0][0]['port_range_max']
      return result
  
  
  def flow_priority_offset(rule, conjunction=False):
      """Calculate flow priority offset from rule.
      Whether the rule belongs to conjunction flows or not is decided
      upon existence of rule['remote_group_id'] but can be overridden
      to be True using the optional conjunction arg.
      """
      conj_offset = 0 if 'remote_group_id' in rule or conjunction else 4
      protocol = rule.get('protocol')
      if protocol is None:
          return conj_offset
  
      if protocol in [n_consts.PROTO_NUM_ICMP, n_consts.PROTO_NUM_IPV6_ICMP]:
          if 'port_range_min' not in rule:
              return conj_offset + 1
          elif 'port_range_max' not in rule:
              return conj_offset + 2
      return conj_offset + 3
  
  
  def create_flows_from_rule_and_port(rule, port, conjunction=False):
      """Create flows from given args.
      For description of the optional conjunction arg, see flow_priority_offset.
      """
      ethertype = rule['ethertype']
      direction = rule['direction']
      dst_ip_prefix = rule.get('dest_ip_prefix')
      src_ip_prefix = rule.get('source_ip_prefix')
  
      flow_template = {
          'priority': 70 + flow_priority_offset(rule, conjunction),
          'dl_type': ovsfw_consts.ethertype_to_dl_type_map[ethertype],
          'reg_port': port.ofport,
      }
  
      if is_valid_prefix(dst_ip_prefix):
          flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[(
              utils.get_ip_version(dst_ip_prefix), n_consts.EGRESS_DIRECTION)]
          ] = dst_ip_prefix
  
      if is_valid_prefix(src_ip_prefix):
          flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[(
              utils.get_ip_version(src_ip_prefix), n_consts.INGRESS_DIRECTION)]
          ] = src_ip_prefix
  
      flows = create_protocol_flows(direction, flow_template, port, rule)
  
      return flows
  
  
  def populate_flow_common(direction, flow_template, port):
      """Initialize common flow fields."""
      if direction == n_consts.INGRESS_DIRECTION:
          flow_template['table'] = ovs_consts.RULES_INGRESS_TABLE
          flow_template['actions'] = "output:{:d},resubmit(,{:d})".format(
              port.ofport,
              ovs_consts.ACCEPTED_INGRESS_TRAFFIC_TABLE)
      elif direction == n_consts.EGRESS_DIRECTION:
          flow_template['table'] = ovs_consts.RULES_EGRESS_TABLE
          # Traffic can be both ingress and egress, check that no ingress rules
          # should be applied
          flow_template['actions'] = 'resubmit(,{:d})'.format(
              ovs_consts.ACCEPT_OR_INGRESS_TABLE)
      return flow_template
  
  
  def create_protocol_flows(direction, flow_template, port, rule):
      flow_template = populate_flow_common(direction,
                                           flow_template.copy(),
                                           port)
      protocol = rule.get('protocol')
      if protocol is not None:
          flow_template['nw_proto'] = protocol
  
      if protocol in [n_consts.PROTO_NUM_ICMP, n_consts.PROTO_NUM_IPV6_ICMP]:
          flows = create_icmp_flows(flow_template, rule)
      else:
          flows = create_port_range_flows(flow_template, rule)
      return flows or [flow_template]
  
  
  def create_port_range_flows(flow_template, rule):
      protocol = ovsfw_consts.REVERSE_IP_PROTOCOL_MAP_WITH_PORTS.get(
          rule.get('protocol'))
      if protocol is None:
          return []
      flows = []
      src_port_match = '{:s}_src'.format(protocol)
      src_port_min = rule.get('source_port_range_min')
      src_port_max = rule.get('source_port_range_max')
      dst_port_match = '{:s}_dst'.format(protocol)
      dst_port_min = rule.get('port_range_min')
      dst_port_max = rule.get('port_range_max')
  
      dst_port_range = []
      if dst_port_min and dst_port_max:
          dst_port_range = utils.port_rule_masking(dst_port_min, dst_port_max)
  
      src_port_range = []
      if src_port_min and src_port_max:
          src_port_range = utils.port_rule_masking(src_port_min, src_port_max)
          for port in src_port_range:
              flow = flow_template.copy()
              flow[src_port_match] = port
              if dst_port_range:
                  for port in dst_port_range:
                      dst_flow = flow.copy()
                      dst_flow[dst_port_match] = port
                      flows.append(dst_flow)
              else:
                  flows.append(flow)
      else:
          for port in dst_port_range:
              flow = flow_template.copy()
              flow[dst_port_match] = port
              flows.append(flow)
  
      return flows
  
  
  def create_icmp_flows(flow_template, rule):
      icmp_type = rule.get('port_range_min')
      if icmp_type is None:
          return
      flow = flow_template.copy()
      flow['icmp_type'] = icmp_type
  
      icmp_code = rule.get('port_range_max')
      if icmp_code is not None:
          flow['icmp_code'] = icmp_code
      return [flow]
  
  
  def _flow_priority_offset_from_conj_id(conj_id):
      "Return a flow priority offset encoded in a conj_id."
      # A base conj_id, which is returned by ConjIdMap.get_conj_id, is a
      # multiple of 8, and we use 2 conj_ids per offset.
      return conj_id % 8 // 2
  
  
  def create_flows_for_ip_address(ip_address, direction, ethertype,
                                  vlan_tag, conj_ids):
      """Create flows from a rule and an ip_address derived from
      remote_group_id
      """
  
      # Group conj_ids per priority.
      conj_id_lists = [[] for i in range(4)]
      for conj_id in conj_ids:
          conj_id_lists[
              _flow_priority_offset_from_conj_id(conj_id)].append(conj_id)
  
      ip_prefix = str(netaddr.IPNetwork(ip_address).cidr)
  
      flow_template = {
          'dl_type': ovsfw_consts.ethertype_to_dl_type_map[ethertype],
          'reg_net': vlan_tag,  # needed for project separation
      }
  
      ip_ver = utils.get_ip_version(ip_prefix)
  
      if direction == n_consts.EGRESS_DIRECTION:
          flow_template['table'] = ovs_consts.RULES_EGRESS_TABLE
      elif direction == n_consts.INGRESS_DIRECTION:
          flow_template['table'] = ovs_consts.RULES_INGRESS_TABLE
  
      flow_template[FLOW_FIELD_FOR_IPVER_AND_DIRECTION[(
          ip_ver, direction)]] = ip_prefix
  
      result = []
      for offset, conj_id_list in enumerate(conj_id_lists):
          if not conj_id_list:
              continue
          flow_template['priority'] = 70 + offset
          result.extend(
              substitute_conjunction_actions([flow_template], 1, conj_id_list))
      return result
  
  
  def create_accept_flows(flow):
      flow['ct_state'] = CT_STATES[0]
      result = [flow.copy()]
      flow['ct_state'] = CT_STATES[1]
      if flow['table'] == ovs_consts.RULES_INGRESS_TABLE:
          flow['actions'] = (
              'ct(commit,zone=NXM_NX_REG{:d}[0..15]),{:s}'.format(
                  ovsfw_consts.REG_NET, flow['actions']))
      result.append(flow)
      return result
  
  
  def substitute_conjunction_actions(flows, dimension, conj_ids):
      result = []
      for flow in flows:
          for i in range(2):
              new_flow = flow.copy()
              new_flow['ct_state'] = CT_STATES[i]
              new_flow['actions'] = ','.join(
                  ["conjunction(%d,%d/2)" % (s + i, dimension)
                   for s in conj_ids])
              result.append(new_flow)
  
      return result
  
  
  def create_conj_flows(port, conj_id, direction, ethertype):
      """Generate "accept" flows for a given conjunction ID."""
      flow_template = {
          'priority': 70 + _flow_priority_offset_from_conj_id(conj_id),
          'conj_id': conj_id,
          'dl_type': ovsfw_consts.ethertype_to_dl_type_map[ethertype],
          # This reg_port matching is for delete_all_port_flows.
          # The matching is redundant as it has been done by
          # conjunction(...,2/2) flows and flows can be summarized
          # without this.
          'reg_port': port.ofport,
      }
      flow_template = populate_flow_common(direction, flow_template, port)
      flows = create_accept_flows(flow_template)
      flows[1]['conj_id'] += 1
      return flows
  