)

  def testGetServiceByProtoWithoutProtocols(self):
    """Ensure services with protocol are not returned when type is specified."""
    self.assertListEqual(self.defs.GetServiceByProto('SVC3', 'tcp'), ['80'])

  def testNetworkComment(self):
    self.assertEqual(self.defs.GetNetAddr('NET1')[0].text, 'network1')

  def testNestedNetworkComment(self):
    self.assertEqual(self.defs.GetNetAddr('NET2')[1].text, 'network1')

  def testUndefinedAddress(self):
    self.assertRaises(naming.UndefinedAddressError, self.defs.GetNetAddr, 'FOO')

  def testNamespaceCollisionError(self):
    badservicedata = []
    badservicedata.append('SVC1 = 80/tcp')
    badservicedata.append('SVC1 = 81/udp')
    testdefs = naming.Naming(None)
    self.assertRaises(naming.NamespaceCollisionError,
                      testdefs.ParseServiceList, badservicedata)

  def testNetworkAddress(self):
    self.assertListEqual(self.defs.GetNetAddr('NET1'),
                         [nacaddr.IPv4('10.0.0.0/8')])

  def testInet6Address(self):
    self.assertListEqual(self.defs.GetNetAddr('BAZ'),
                         [nacaddr.IPv6('::FFFF:FFFF:FFFF:FFFF'),
                          nacaddr.IPv6('::1/128')])

  def testMixedAddresses(self):
    self.assertListEqual(self.defs.GetNetAddr('BING'),
                         [nacaddr.IPv4('10.0.0.0/8'),
                          nacaddr.IPv6('::FFFF:FFFF:FFFF:FFFF')])
    # same thing but letting nacaddr decide which v4 or v6.
    self.assertListEqual(self.defs.GetNetAddr('BING'),
                         [nacaddr.IP('10.0.0.0/8'),
                          nacaddr.IP('::FFFF:FFFF:FFFF:FFFF')])

  def testNestedServices(self):
    self.assertListEqual(self.defs.GetServiceByProto('SVC6', 'tcp'),
                         ['80', '82', '90'])

  def testServiceParents(self):
    """SVC6 contains SVC5 which contains TCP_90 which contains 90/tcp."""
    self.assertListEqual(self.defs.GetServiceParents('90/tcp'),
                         ['TCP_90', 'SVC5', 'SVC6'])

  def testNetParents(self):
    """BIN & NET2 contain NET1, BING & BAZ contain FOO_V6."""
    self.assertListEqual(sorted(self.defs.GetNetParents('NET1')),
                         ['BING', 'NET2'])
    self.assertListEqual(sorted(self.defs.GetNetParents('FOO_V6')),
                         ['BAZ', 'BING'])

  def testGetIpParents(self):
    """Ensure GetIpParents returns proper results."""
    self.assertListEqual(self.defs.GetIpParents('10.11.12.13/32'),
                         ['BING', 'NET1', 'NET2'])

  def testUndefinedTokenNesting(self):
    bad_servicedata = ['FOO = 7/tcp BAR']
    bad_networkdata = ['NETGROUP = 10.0.0.0/8 FOOBAR']
    baddefs = naming.Naming(None)
    baddefs.ParseServiceList(bad_servicedata)
    baddefs.ParseNetworkList(bad_networkdata)
    self.assertRaises(naming.UndefinedServiceError,
                      baddefs._CheckUnseen, 'services')
    self.assertRaises(naming.UndefinedAddressError,
                      baddefs._CheckUnseen, 'networks')

  def testParseNetFile(self):
    filedefs = naming.Naming(None)
    data = ['FOO = 127.0.0.1 # some network\n']
    filedefs._ParseFile(data, 'networks')
    self.assertEqual(filedefs.GetNetAddr('FOO'), [nacaddr.IPv4('127.0.0.1')])

  def testParseServiceFile(self):
    filedefs = naming.Naming(None)
    data = ['HTTP = 80/tcp\n']
    filedefs._ParseFile(data, 'services')
    self.assertEqual(filedefs.GetService('HTTP'), ['80/tcp'])

  def testServiceIncorrectSyntax(self):
    badservicedata = []
    badservicedata.append('SVC1 = 80//tcp 80/udp')
    badservicedata.append('SVC2 = 81/tcp')
    testdefs = naming.Naming(None)
    self.assertRaises(naming.NamingSyntaxError,
                      testdefs.ParseServiceList, badservicedata)

  def testGetNetChildrenSingle(self):
    expected = ['NET1']
    self.assertEqual(expected, self.defs.GetNetChildren('NET2'))

  def testGetNetChildrenMulti(self):
    expected = ['FOO_V6', 'BAR_V6']
    self.assertEqual(expected, self.defs.GetNetChildren('BAZ'))

  def testGetNetChildrenQueryNotExist(self):
    self.assertEqual([], self.defs.GetNetChildren('IDONOTEXIST'))

  def testGetNetChildrenNoChild(self):
    self.assertEqual([], self.defs.GetNetChildren('NET1'))


if __name__ == '__main__':
  absltest.main()


# tests/lib/nftables_test.py

# Copyright 2014 Google 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.

"""Unittest for Nftables rendering module."""

import datetime
from absl.testing import absltest
from unittest import mock

from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import nftables
from capirca.lib import policy


BAD_HEADER = """
header {
  target:: nftables %s
}
"""

GOOD_HEADER_1 = """
header {
  target:: nftables chain_name input 0 inet
}
"""

GOOD_HEADER_2 = """
header {
  target:: nftables chain_name input 0 inet6
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  action:: accept
}
"""

IPV6_TERM_1 = """
term inet6-icmp {
  protocol:: icmpv6
  icmp-type:: destination-unreachable time-exceeded echo-reply
  action:: deny
}
"""

IPV6_TERM_2 = """
term inet6-icmp {
  action:: deny
}
"""

EXPIRED_TERM = """
term is_expired {
    expiration:: 2001-01-01
    action:: accept
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  source-address:: SOURCE_NETWORK
  destination-address:: DESTINATION_NETWORK
  action:: accept
}
"""

GOOD_TERM_3 = """
term good-term-3 {
  source-address:: SOURCE_NETWORK
  destination-address:: DESTINATION_NETWORK
  action:: %s
}
"""

GOOD_TERM_4 = """
term good-term-4 {
  action:: accept
  comment:: "comment first line"
  comment:: "comment second line"
  owner:: owner@enterprise.com
}
"""

GOOD_TERM_5 = """
term good-term-5 {
  protocol:: ah esp
  action:: accept
}
"""

GOOD_TERM_6 = """
term good-term-6 {
  protocol:: ah
  action:: accept
}
"""

GOOD_TERM_7 = """
term good-term-7 {
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_8 = """
term good-term-8 {
  source-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_9 = """
term good-term-9 {
  protocol:: icmp
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

GOOD_TERM_10 = """
term good-term-10 {
  verbatim:: nftables "mary had a little lamb"
  verbatim:: cisco "mary had second lamb"
  verbatim:: juniper "mary had third lamb"
}
"""

GOOD_TERM_11 = """
term good-term-11 {
  source-address:: SOURCE_NETWORK
  source-exclude:: SOURCE_EXCLUDE_NETWORK
  destination-address:: DESTINATION_NETWORK
  destination-exclude:: DESTINATION_EXCLUDE_NETWORK
  action:: accept
}
"""

GOOD_TERM_12 = """
term good-term-12 {
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_13 = """
term good-term-13 {
  protocol:: icmp
  action:: accept
  logging:: true
}
"""

GOOD_TERM_14 = """
term good-term-14 {
  protocol:: icmp
  action:: accept
  log_name:: "my log prefix"
}
"""

GOOD_TERM_15 = """
term good-term-15 {
  protocol:: icmp
  action:: accept
  logging:: true
  log_name:: "my log prefix"
}
"""
GOOD_TERM_16 = """
term good-term-16 {
  protocol:: icmp
  action:: accept
  counter:: string_content_unused
}
"""

GOOD_TERM_17 = """
term good-term-17 {
  action:: accept
  comment:: "%(long_line)s:"
}
""" % {'long_line': 'A' * 128}

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'logging',
    'log_name',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {
        'accept',
        'deny',
        'next',
        'reject',
        'reject-with-tcp-rst',
    },
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class NftablesTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.mock_naming = mock.MagicMock()

  def testBadHeader(self):
    cases = [
        'chain_name input 0 inet extraneous_target_option',
        'chain_name input 0 unsupported_af',
        'chain_name input not_an_int_priority',
        'chain_name invalid_hook_name 0',
        'chain_name input'
        'chain_name',
        '',
    ]
    for case in cases:
      logging.info('Testing bad header case %s.', case)
      header = BAD_HEADER % case
      pol = policy.ParsePolicy(header + GOOD_TERM_1, self.mock_naming)
      self.assertRaises(nftables.InvalidTargetOption,
                        nftables.Nftables.__init__,
                        nftables.Nftables.__new__(nftables.Nftables),
                        pol, EXP_INFO)

  def testBadAddressFamily(self):
    cases = [
        'chain_name input 0 mixed',
    ]
    for case in cases:
      logging.info('Testing bad address family case %s.', case)
      header = BAD_HEADER % case
      pol = policy.ParsePolicy(header + GOOD_TERM_1, self.mock_naming)
      self.assertRaises(aclgenerator.UnsupportedAFError,
                        nftables.Nftables.__init__,
                        nftables.Nftables.__new__(nftables.Nftables),
                        pol, EXP_INFO)

  def testGoodHeader(self):
    nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                         self.mock_naming), EXP_INFO)
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1 +
                                                   GOOD_HEADER_2 + IPV6_TERM_2,
                                                   self.mock_naming),
                                EXP_INFO))
    self.assertIn('flush table ip table_filter', nft)
    self.assertIn(
        'table ip table_filter {\n\tchain chain_name {\n\t\ttype filter '
        'hook input priority 0;\n\t\taccept\n\t}\n}', nft)
    self.assertIn('flush table ip6 table_filter', nft)
    self.assertIn(
        'table ip6 table_filter {\n\tchain chain_name {\n\t\ttype filter '
        'hook input priority 0;\n\t\tdrop\n\t}\n}', nft)

  @mock.patch.object(logging, 'warning')
  def testExpired(self, mock_logging_warn):
    nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + EXPIRED_TERM,
                                         self.mock_naming), EXP_INFO)
    mock_logging_warn.assert_called_once_with('Term %s in policy %s is expired '
                                              'and will not be rendered.',
                                              'is_expired', 'chain_name')

  @mock.patch.object(logging, 'info')
  def testExpiring(self, mock_logging_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + EXPIRING_TERM %
                                         exp_date.strftime('%Y-%m-%d'),
                                         self.mock_naming), EXP_INFO)
    mock_logging_info.assert_called_once_with('Term %s in policy %s '
                                              'expires in less than %d weeks.',
                                              'is_expiring', 'chain_name',
                                              EXP_INFO)

  @mock.patch.object(logging, 'debug')
  def testIcmpv6InetMismatch(self, mock_logging_debug):
    str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + IPV6_TERM_1,
                                             self.mock_naming), EXP_INFO))
    mock_logging_debug.assert_called_once_with('Term inet6-icmp will not be '
                                               'rendered, as it has '
                                               'icmpv6 match specified '
                                               'but the ACL is of inet address '
                                               'family.')

  def testSingleSourceDestIp(self):
    source_network = [nacaddr.IPv4('172.16.0.0/24')]
    destination_network = [nacaddr.IPv4('10.0.0.0/24')]
    self.mock_naming.GetNetAddr.side_effect = [source_network,
                                               destination_network]
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('ip saddr 172.16.0.0/24 ip daddr 10.0.0.0/24', nft)

  def testMultipleSourceDestIp(self):
    source_network = [nacaddr.IPv4('172.16.0.0/24'),
                      nacaddr.IPv4('172.16.2.0/24')]
    destination_network = [nacaddr.IPv4('10.0.0.0/24'),
                           nacaddr.IPv4('10.0.2.0/24')]
    self.mock_naming.GetNetAddr.side_effect = [source_network,
                                               destination_network]
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('ip saddr { 172.16.0.0/24, 172.16.2.0/24} ip daddr '
                  '{ 10.0.0.0/24, 10.0.2.0/24}', nft)

  def testSingleProtocol(self):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_5,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('ip protocol { ah, esp}', nft)

  def testMultiProtocol(self):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_6,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('ip protocol ah', nft)

  def testSingleDport(self):
    destination_ports = ['25']
    self.mock_naming.GetServiceByProto.return_value = destination_ports
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_7,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('dport 25', nft)

  def testMultiDport(self):
    destination_ports = ['25', '80', '6610', '6611', '6612']
    self.mock_naming.GetServiceByProto.return_value = destination_ports
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_7,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('dport { 25, 80, 6610-6612}', nft)

  def testSingleSport(self):
    source_ports = ['25']
    self.mock_naming.GetServiceByProto.return_value = source_ports
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_8,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('sport 25', nft)

  def testMultiSport(self):
    source_ports = ['25', '80', '6610', '6611', '6612']
    self.mock_naming.GetServiceByProto.return_value = source_ports
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_8,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('sport { 25, 80, 6610-6612}', nft)

  def testIcmpType(self):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_9,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('icmp type { echo-reply, echo-request}', nft)

  def testAction(self):
    cases = {'accept': 'accept',
             'deny': 'drop',
             'reject': 'reject',
             'next': 'continue',
             'reject-with-tcp-rst': 'reject with tcp reset'}
    for case in cases:
      logging.info('Testing action case %s.', case)
      nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 +
                                                     GOOD_TERM_3 % case,
                                                     self.mock_naming),
                                  EXP_INFO))
      self.assertIn(cases[case], nft)

  def testCommentOwner(self):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_4,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('comment "comment first line comment second line '
                  'Owner: owner@enterprise.com"', nft)

  @mock.patch.object(logging, 'warning')
  def testCommentTruncate(self, mock_logging_warn):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_17,
                                                   self.mock_naming), EXP_INFO))
    mock_logging_warn.assert_called_once_with(
        'Term %s in policy is too long (>%d characters) and will be'
        ' truncated', 'good-term-17', nftables.Term.MAX_CHARACTERS)
    # Ensure that the truncate did happen and stripped off the ':'
    self.assertIn('comment "%(long_line)s' % {'long_line': 'A' * 127}, nft)

  def testLogTerm(self):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_13,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn(' icmp log accept', nft)

  def testLogNameTerm(self):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_14,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('log prefix "my log prefix: " ', nft)

  def testLogAndLogNameTerm(self):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_15,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('log prefix "my log prefix: " ', nft)

  def testCounterTerm(self):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_16,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn(' icmp counter accept', nft)

  def testVerbatimTerm(self):
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_10,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('mary had a little lamb', nft)
    # check if another platforms verbatim shows up
    self.assertNotIn('mary had a second lamb', nft)
    self.assertNotIn('mary had a third lamb', nft)

  def testSourceDestExclude(self):
    source_network = [nacaddr.IPv4('192.168.0.0/24')]
    source_exclude_network = [nacaddr.IPv4('192.168.0.0/27')]
    destination_network = [nacaddr.IPv4('10.0.0.0/24')]
    destination_exclude_network = [nacaddr.IPv4('10.0.0.0/27')]
    self.mock_naming.GetNetAddr.side_effect = [source_network,
                                               source_exclude_network,
                                               destination_network,
                                               destination_exclude_network]
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_11,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('ip saddr { 192.168.0.32/27, 192.168.0.64/26, '
                  '192.168.0.128/25}', nft)
    self.assertIn('ip daddr { 10.0.0.32/27, 10.0.0.64/26, '
                  '10.0.0.128/25}', nft)

  def testSourceDestExcludeFromAllIps(self):
    source_network = []
    source_exclude_network = [nacaddr.IPv4('192.168.0.0/27')]
    destination_network = []
    destination_exclude_network = [nacaddr.IPv4('10.0.0.0/27')]
    self.mock_naming.GetNetAddr.side_effect = [source_network,
                                               source_exclude_network,
                                               destination_network,
                                               destination_exclude_network]
    nft = str(nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_11,
                                                   self.mock_naming), EXP_INFO))
    self.assertIn('ip saddr { 0.0.0.0/1, 128.0.0.0/2, 192.0.0.0/9, '
                  '192.128.0.0/11, 192.160.0.0/13, 192.168.0.32/27, '
                  '192.168.0.64/26, 192.168.0.128/25, 192.168.1.0/24, '
                  '192.168.2.0/23, 192.168.4.0/22, 192.168.8.0/21, '
                  '192.168.16.0/20, 192.168.32.0/19, 192.168.64.0/18, '
                  '192.168.128.0/17, 192.169.0.0/16, 192.170.0.0/15, '
                  '192.172.0.0/14, 192.176.0.0/12, 192.192.0.0/10, '
                  '193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, 200.0.0.0/5, '
                  '208.0.0.0/4, 224.0.0.0/3}', nft)
    self.assertIn('ip daddr { 0.0.0.0/5, 8.0.0.0/7, 10.0.0.32/27, '
                  '10.0.0.64/26, 10.0.0.128/25, 10.0.1.0/24, 10.0.2.0/23, '
                  '10.0.4.0/22, 10.0.8.0/21, 10.0.16.0/20, 10.0.32.0/19, '
                  '10.0.64.0/18, 10.0.128.0/17, 10.1.0.0/16, 10.2.0.0/15, '
                  '10.4.0.0/14, 10.8.0.0/13, 10.16.0.0/12, 10.32.0.0/11, '
                  '10.64.0.0/10, 10.128.0.0/9, 11.0.0.0/8, 12.0.0.0/6, '
                  '16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2, 128.0.0.0/1}', nft)

  def testBuildTokens(self):
    pol1 = nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                                self.mock_naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = nftables.Nftables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_12,
                                                self.mock_naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


if __name__ == '__main__':
  absltest.main()


# tests/lib/nsxv_functtest.py

# Copyright 2015 The Capirca Project Authors 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.
#
"""Functional test class for nsxv.py."""

import copy
import optparse
from absl.testing import absltest
from xml.etree import ElementTree as ET

from capirca.lib import naming
from capirca.lib import nsxv
from capirca.lib import policy
from capirca.tests.lib import nsxv_mocktest


class NsxvFunctionalTest(absltest.TestCase):
  """Functional testing for NSXV."""

  def setUp(self):
    """Call before every test case."""
    super().setUp()
    parser = optparse.OptionParser()
    parser.add_option(
        '-d',
        '--def',
        dest='definitions',
        help='definitions directory',
        default='../def')
    (FLAGS, args) = _parser.parse_args()
    self.defs = naming.Naming(FLAGS.definitions)

  def tearDown(self):
    super().tearDown()
    pass

  def runTest(self):
    pass

  def test_nsxv_policy(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY, self.defs)
    exp_info = 2
    nsx = copy.deepcopy(pol)
    fw = nsxv.Nsxv(nsx, exp_info)
    output = str(fw)

    # parse the xml
    root = ET.fromstring(output)
    # check section name
    section_name = {'id': '1007', 'name': 'POLICY_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'reject-imap-requests')
    self.assertEqual(root.find('./rule/action').text, 'reject')

    # check IPV4 destination
    exp_destaddr = ['200.1.1.4/31']

    for destination in root.findall('./rule/destinations/destination'):
      self.assertEqual((destination.find('type').text), 'Ipv4Address')
      value = (destination.find('value').text)
      if value not in exp_destaddr:
        self.fail('IPv4Address destination not found in test_nsxv_str()')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 6)

    # check destination port
    destination_port = root.find('./rule/services/service/destinationPort').text
    self.assertEqual(destination_port, '143')

  def test_nsxv_nosectiondid(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY_NO_SECTION_ID, self.defs)
    exp_info = 2
    nsx = copy.deepcopy(pol)
    fw = nsxv.Nsxv(nsx, exp_info)
    output = str(fw)
    # parse the xml
    root = ET.fromstring(output)
    # check section name
    section_name = {'name': 'POLICY_NO_SECTION_ID_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'accept-icmp')
    self.assertEqual(root.find('./rule/action').text, 'allow')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 1)

  def test_nsxv_nofiltertype(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY_NO_FILTERTYPE, self.defs)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError, nsxv.Nsxv(pol, 2))

  def test_nsxv_incorrectfiltertype(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY_INCORRECT_FILTERTYPE,
                             self.defs)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError, nsxv.Nsxv(pol, 2))

  def test_nsxv_optionkywd(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY_OPTION_KYWD, self.defs)
    self.assertRaises(nsxv.NsxvAclTermError, str(nsxv.Nsxv(pol, 2)))

  if __name__ == '__main__':
    absltest.main()


# tests/lib/nsxv_mocktest.py

# Copyright 2015 The Capirca Project Authors 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.
#
"""Nsxv Mock Test terms for nsxv module."""



INET_TERM = """\
  term permit-mail-services {
    destination-address:: MAIL_SERVERS
    protocol:: tcp
    destination-port:: MAIL_SERVICES
    action:: accept
  }
  """

INET6_TERM = """\
  term test-icmpv6 {
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

INET_FILTER = """\
  header {
    comment:: "Sample inet NSXV filter"
    target:: nsxv INET_FILTER_NAME inet
  }

  term allow-ntp-request {
    comment::"Allow ntp request"
    source-address:: NTP_SERVERS
    source-port:: NTP
    destination-address:: INTERNAL
    destination-port:: NTP
    protocol:: udp
    action:: accept
  }
  """

INET6_FILTER = """\
  header {
    comment:: "Sample inet6 NSXV filter"
    target:: nsxv INET6_FILTER_NAME inet6
  }

  term test-icmpv6 {
    #destination-address:: WEB_SERVERS
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

MIXED_FILTER = """\
  header {
    comment:: "Sample mixed NSXV filter"
    target:: nsxv MIXED_FILTER_NAME mixed 1009
  }

  term accept-to-honestdns {
    comment:: "Allow name resolution using honestdns."
    destination-address:: GOOGLE_DNS
    destination-port:: DNS
    protocol:: udp
    action:: accept
  }
  """

POLICY = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_NAME inet 1007
  }

  term reject-imap-requests {
    destination-address:: MAIL_SERVERS
    destination-port:: IMAP
    protocol:: tcp
    action:: reject-with-tcp-rst
  }
  """

POLICY_NO_SECTION_ID = """\
  header {
    comment:: "NSXV filter without section id"
    target:: nsxv POLICY_NO_SECTION_ID_NAME inet
  }
  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

POLICY_NO_FILTERTYPE = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_NO_FILTERTYPE_NAME
  }
  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

POLICY_INCORRECT_FILTERTYPE = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_INCORRECT_FILTERTYPE_NAME inet1
  }
  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

POLICY_OPTION_KYWD = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_OPTION_KYWD_NAME inet 1009
  }
  term accept-bgp-replies {
    comment:: "Allow inbound replies to BGP requests."
    source-port:: BGP
    protocol:: tcp
    option:: tcp-established
    action:: accept
  }
  """


# tests/lib/nsxv_test.py

# Copyright 2016 Google 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.
"""UnitTest class for nsxv.py."""

from absl.testing import absltest
from unittest import mock
from xml.etree import ElementTree as ET

from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import nsxv
from capirca.lib import policy


INET_TERM = """\
  term permit-mail-services {
    destination-address:: MAIL_SERVERS
    protocol:: tcp
    destination-port:: MAIL_SERVICES
    action:: accept
  }
  """

INET6_TERM = """\
  term test-icmpv6 {
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

INET_FILTER = """\
  header {
    comment:: "Sample inet NSXV filter"
    target:: nsxv INET_FILTER_NAME inet
  }

  term allow-ntp-request {
    comment::"Allow ntp request"
    source-address:: NTP_SERVERS
    source-port:: NTP
    destination-address:: INTERNAL
    destination-port:: NTP
    protocol:: udp
    action:: accept
  }
  """

INET_FILTER_2 = """\
  header {
    comment:: "Sample inet NSXV filter"
    target:: nsxv INET_FILTER2_NAME inet
  }

  term allow-ntp-request {
    comment::"Allow ntp request"
    source-address:: NTP_SERVERS
    source-port:: NTP
    destination-address:: INTERNAL
    destination-port:: NTP
    protocol:: udp
    policer:: batman
    action:: accept
  }
  """

INET_FILTER_WITH_ESTABLISHED = """\
  header {
    comment:: "Sample inet NSXV filter"
    target:: nsxv INET_FILTER_WITH_ESTABLISHED_NAME inet
  }

  term allow-ntp-request {
    comment::"Allow ntp request"
    source-address:: NTP_SERVERS
    source-port:: NTP
    destination-address:: INTERNAL
    destination-port:: NTP
    protocol:: udp
    option:: tcp-established
    policer:: batman
    action:: accept
  }
  """
MIXED_HEADER = """\
  header {
    comment:: "Sample mixed NSXV filter"
    target:: nsxv MIXED_HEADER_NAME mixed
  }

"""

INET_HEADER = """\
  header {
    comment:: "Sample mixed NSXV filter"
    target:: nsxv INET_HEADER_NAME inet
  }

"""

MIXED_FILTER_INET_ONLY = MIXED_HEADER + INET_TERM

INET_FILTER_NO_SOURCE = INET_HEADER + INET_TERM

INET6_FILTER = """\
  header {
    comment:: "Sample inet6 NSXV filter"
    target:: nsxv INET6_FILTER_NAME inet6
  }

  term test-icmpv6 {
    #destination-address:: WEB_SERVERS
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

MIXED_FILTER = """\
  header {
    comment:: "Sample mixed NSXV filter"
    target:: nsxv MIXED_FILTER_NAME mixed
  }

  term accept-to-honestdns {
    comment:: "Allow name resolution using honestdns."
    destination-address:: GOOGLE_DNS
    destination-port:: DNS
    protocol:: udp
    action:: accept
  }
  """

POLICY = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_NAME inet
  }

  term reject-imap-requests {
    destination-address:: MAIL_SERVERS
    destination-port:: IMAP
    protocol:: tcp
    action:: reject-with-tcp-rst
  }
  """

POLICY_WITH_SECURITY_GROUP = """\
  header {
    comment:: "Sample filter with Security Group"
    target:: nsxv POLICY_WITH_SECURITY_GROUP_NAME inet 1010 securitygroup \
    securitygroup-Id
  }

  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

HEADER_WITH_SECTIONID = """\
  header {
    comment:: "Sample NSXV filter1"
    target:: nsxv HEADER_WITH_SECTIONID_NAME inet 1009
  }
  """

HEADER_WITH_SECURITYGROUP = """\
  header {
    comment:: "Sample NSXV filter2"
    target:: nsxv HEADER_WITH_SECURITYGROUP_NAME inet6 securitygroup \
    securitygroup-Id1
  }
  """

BAD_HEADER = """\
  header {
    comment:: "Sample NSXV filter3"
    target:: nsxv BAD_HEADER_NAME inet 1011 securitygroup
  }
  """

BAD_HEADER_1 = """\
  header {
    comment:: "Sample NSXV filter4"
    target:: nsxv BAD_HEADER_1_NAME 1012
  }
  """

BAD_HEADER_2 = """\
  header {
    comment:: "Sample NSXV filter5"
    target:: nsxv BAD_HEADER_2_NAME inet securitygroup
  }
  """

BAD_HEADER_3 = """\
  header {
    comment:: "Sample NSXV filter6"
    target:: nsxv BAD_HEADER_3_NAME
  }
  """

BAD_HEADER_4 = """\
  header {
    comment:: "Sample NSXV filter7"
    target:: nsxv BAD_HEADER_3_NAME inet 1234 securitygroup securitygroup \
    securitygroupId1
  }
  """

TERM = """\
  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

MIXED_TO_V4 = """\
  term mixed_to_v4 {
    source-address:: GOOGLE_DNS
    destination-address:: INTERNAL
    protocol:: tcp udp
    action:: accept
  }
  """

V4_TO_MIXED = """\
  term v4_to_mixed {
    source-address:: INTERNAL
    destination-address:: GOOGLE_DNS
    protocol:: tcp udp
    action:: accept
  }
  """

MIXED_TO_V6 = """\
  term mixed_to_v6 {
    source-address:: GOOGLE_DNS
    destination-address:: SOME_HOST
    action:: accept
  }
  """

V6_TO_MIXED = """\
  term v6_to_mixed {
    source-address:: SOME_HOST
    destination-address:: GOOGLE_DNS
    action:: accept
  }
  """

MIXED_TO_MIXED = """\
  term mixed_to_mixed {
    source-address:: GOOGLE_DNS
    destination-address:: GOOGLE_DNS
    action:: accept
  }
  """

MIXED_TO_ANY = """\
  term mixed_to_any {
    source-address:: GOOGLE_DNS
    action:: accept
  }
  """

ANY_TO_MIXED = """\
  term any_to_mixed {
    destination-address:: GOOGLE_DNS
    action:: accept
  }
  """

V4_TO_V4 = """\
  term v4_to_v4 {
    source-address:: NTP_SERVERS
    destination-address:: INTERNAL
    action:: accept
  }
  """

V6_TO_V6 = """\
  term v6_to_v6 {
    source-address:: SOME_HOST
    destination-address:: SOME_HOST
    action:: accept
  }
  """

V4_TO_V6 = """\
  term v4_to_v6 {
    source-address:: INTERNAL
    destination-address:: SOME_HOST
    action:: accept
  }
  """

V6_TO_V4 = """\
  term v6_to_v4 {
    source-address:: SOME_HOST
    destination-address:: INTERNAL
    action:: accept
  }
  """

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'option',
    'platform',
    'platform_exclude',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2
_PLATFORM = 'nsxv'


class TermTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testInitForinet(self):
    """Test for Term._init_."""
    inet_term = nsxv.Term(INET_TERM, 'inet')
    self.assertEqual(inet_term.af, 4)
    self.assertEqual(inet_term.filter_type, 'inet')

  def testInitForinet6(self):
    """Test for Term._init_."""
    inet6_term = nsxv.Term(INET6_TERM, 'inet6', None, 6)
    self.assertEqual(inet6_term.af, 6)
    self.assertEqual(inet6_term.filter_type, 'inet6')

  def testServiceToStr(self):
    """Test for Term._ServiceToStr."""

    proto = 6
    icmp_types = []
    dports = [(1024, 65535)]
    spots = [(123, 123)]
    nsxv_term = nsxv.Term(INET_TERM, 'inet')
    service = nsxv_term._ServiceToString(proto, spots, dports, icmp_types)
    self.assertEqual(service, '<service><protocol>6</protocol><sourcePort>'
                     '123</sourcePort><destinationPort>1024-65535'
                     '</destinationPort></service>')

  def testStrForinet(self):
    """Test for Term._str_."""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    pol = policy.ParsePolicy(INET_FILTER, self.naming, False)
    af = 4
    for _, terms in pol.filters:
      nsxv_term = nsxv.Term(terms[0], af)
      rule_str = nsxv.Term.__str__(nsxv_term)
    # parse xml rule and check if the values are correct
    root = ET.fromstring(rule_str)
    # check name and action
    self.assertEqual(root.find('name').text, 'allow-ntp-request')
    self.assertEqual(root.find('action').text, 'allow')

    # check source address
    exp_sourceaddr = ['10.0.0.1', '10.0.0.2']
    source_address = root.findall('./sources/source')
    self.assertNotEqual(len(source_address), 0)
    for source in source_address:
      self.assertEqual((source.find('type').text), 'Ipv4Address')
      value = (source.find('value').text)
      if value not in exp_sourceaddr:
        self.fail('IPv4Address source address not found in test_str_forinet()')

    # check destination address
    exp_destaddr = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
    destination_address = root.findall('./destinations/destination')
    self.assertNotEqual(len(destination_address), 0)
    for destination in destination_address:
      self.assertEqual((destination.find('type').text), 'Ipv4Address')
      value = (destination.find('value').text)
      if value not in exp_destaddr:
        self.fail('IPv4Address destination not found in test_str_forinet()')

    # check protocol
    protocol = int(root.find('./services/service/protocol').text)
    self.assertEqual(protocol, 17)

    # check source port
    source_port = root.find('./services/service/sourcePort').text
    self.assertEqual(source_port, '123')

    # check destination port
    destination_port = root.find('./services/service/destinationPort').text
    self.assertEqual(destination_port, '123')

    # check notes
    notes = root.find('notes').text
    self.assertEqual(notes, 'Allow ntp request')

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('NTP', 'udp')] * 2)

  def testStrForinet6(self):
    """Test for Term._str_."""
    pol = policy.ParsePolicy(INET6_FILTER, self.naming, False)
    af = 6
    filter_type = 'inet6'
    for _, terms in pol.filters:
      nsxv_term = nsxv.Term(terms[0], filter_type, None, af)
      rule_str = nsxv.Term.__str__(nsxv_term)

    # parse xml rule and check if the values are correct
    root = ET.fromstring(rule_str)
    # check name and action
    self.assertEqual(root.find('name').text, 'test-icmpv6')
    self.assertEqual(root.find('action').text, 'allow')

    # check protocol and sub protocol
    exp_subprotocol = [128, 129]
    for service in root.findall('./services/service'):
      protocol = int(service.find('protocol').text)
      self.assertEqual(protocol, 58)

      sub_protocol = int(service.find('subProtocol').text)
      if sub_protocol not in exp_subprotocol:
        self.fail('subProtocol not matched in test_str_forinet6()')

  def testTranslatePolicy(self):
    """Test for Nsxv.test_TranslatePolicy."""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    pol = policy.ParsePolicy(INET_FILTER, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'inet')
      self.assertEqual(filter_list, ['inet'])
      self.assertEqual(len(terms), 1)

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('NTP', 'udp')] * 2)

  def testTranslatePolicyMixedFilterInetOnly(self):
    """Test for Nsxv.test_TranslatePolicy. Testing Mixed filter with inet."""
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(MIXED_FILTER_INET_ONLY, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'mixed')
      self.assertEqual(filter_list, ['mixed'])
      self.assertEqual(len(terms), 1)
      self.assertIn('10.0.0.0/8', str(terms[0]))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('MAIL_SERVERS')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('MAIL_SERVICES', 'tcp')] * 1)

  def testTranslatePolicyMixedFilterInet6Only(self):
    """Test for Nsxv.test_TranslatePolicy. Testing Mixed filter with inet6."""
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001:4860:4860::8844')]

    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(MIXED_FILTER_INET_ONLY, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'mixed')
      self.assertEqual(filter_list, ['mixed'])
      self.assertEqual(len(terms), 1)
      self.assertIn('2001:4860:4860::8844', str(terms[0]))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('MAIL_SERVERS')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('MAIL_SERVICES', 'tcp')] * 1)

  def testTranslatePolicyMixedFilterInetMixed(self):
    """Test for Nsxv.test_TranslatePolicy. Testing Mixed filter with mixed."""
    self.naming.GetNetAddr.return_value = [
        nacaddr.IP('2001:4860:4860::8844'),
        nacaddr.IP('10.0.0.0/8')
    ]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(MIXED_FILTER_INET_ONLY, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'mixed')
      self.assertEqual(filter_list, ['mixed'])
      self.assertEqual(len(terms), 1)
      self.assertIn('2001:4860:4860::8844', str(terms[0]))
      self.assertIn('10.0.0.0/8', str(terms[0]))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('MAIL_SERVERS')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('MAIL_SERVICES', 'tcp')] * 1)

  def testTranslatePolicyWithEstablished(self):
    """Test for Nsxv.test_TranslatePolicy."""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    pol = policy.ParsePolicy(INET_FILTER_WITH_ESTABLISHED, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'inet')
      self.assertEqual(filter_list, ['inet'])
      self.assertEqual(len(terms), 1)

      self.assertNotIn('<sourcePort>123</sourcePort><destinationPort>123',
                       str(terms[0]))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('NTP', 'udp')] * 2)

  def testNsxvStr(self):
    """Test for Nsxv._str_."""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'),
         nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]
    self.naming.GetServiceByProto.return_value = ['53']

    pol = policy.ParsePolicy(MIXED_FILTER, self.naming, False)
    target = nsxv.Nsxv(pol, EXP_INFO)

    # parse the output and seperate sections and comment
    section_tokens = str(target).split('<section')
    sections = []

    for sec in section_tokens:
      section = sec.replace('name=', '<section name=')
      sections.append(section)
    # parse the xml
    # Checking comment tag
    comment = sections[0]
    if 'Id' not in comment:
      self.fail('Id missing in xml comment in test_nsxv_str()')
    if 'Date' not in comment:
      self.fail('Date missing in xml comment in test_nsxv_str()')
    if 'Revision' not in comment:
      self.fail('Revision missing in xml comment in test_nsxv_str()')

    root = ET.fromstring(sections[1])
    # check section name
    section_name = {'name': 'MIXED_FILTER_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'accept-to-honestdns')
    self.assertEqual(root.find('./rule/action').text, 'allow')

    # check IPV4 and IPV6 destinations
    exp_ipv4dest = ['8.8.4.4', '8.8.8.8']
    exp_ipv6dest = ['2001:4860:4860::8844', '2001:4860:4860::8888']

    destination_address = root.findall('./rule/destinations/destination')
    self.assertNotEqual(len(destination_address), 0)
    for destination in destination_address:
      addr_type = destination.find('type').text
      value = (destination.find('value').text)

      if 'Ipv4Address' in addr_type:
        if value not in exp_ipv4dest:
          self.fail('IPv4Address not found in test_nsxv_str()')
      else:
        if value not in exp_ipv6dest:
          self.fail('IPv6Address not found in test_nsxv_str()')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 17)

    # check destination port
    destination_port = root.find('./rule/services/service/destinationPort').text
    self.assertEqual(destination_port, '53')

    # check notes
    notes = root.find('./rule/notes').text
    self.assertEqual(notes, 'Allow name resolution using honestdns.')

    self.naming.GetNetAddr.assert_called_once_with('GOOGLE_DNS')
    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'udp')

  def testNsxvStrWithSecurityGroup(self):
    """Test for Nsxv._str_."""
    pol = policy.ParsePolicy(POLICY_WITH_SECURITY_GROUP, self.naming, False)
    target = nsxv.Nsxv(pol, EXP_INFO)

    # parse the output and seperate sections and comment
    section_tokens = str(target).split('<section')
    sections = []

    for sec in section_tokens:
      section = sec.replace('id=', '<section id=')
      sections.append(section)
    # parse the xml
    # Checking comment tag
    comment = sections[0]
    if 'Id' not in comment:
      self.fail('Id missing in xml comment in test_nsxv_str()')
    if 'Date' not in comment:
      self.fail('Date missing in xml comment in test_nsxv_str()')
    if 'Revision' not in comment:
      self.fail('Revision missing in xml comment in test_nsxv_str()')

    root = ET.fromstring(sections[1])
    # check section name
    section_name = {'id': '1010', 'name': 'POLICY_WITH_SECURITY_GROUP_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'accept-icmp')
    self.assertEqual(root.find('./rule/action').text, 'allow')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 1)

    # check appliedTo
    applied_to = root.find('./rule/appliedToList/appliedTo/value').text
    self.assertEqual(applied_to, 'securitygroup-Id')

  def testBuildTokens(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']
    pol1 = nsxv.Nsxv(policy.ParsePolicy(INET_FILTER, self.naming), 2)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])

  def testBuildWarningTokens(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    pol1 = nsxv.Nsxv(policy.ParsePolicy(INET_FILTER_2, self.naming), 2)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])

  def testParseFilterOptionsCase1(self):
    pol = nsxv.Nsxv(policy.ParsePolicy(HEADER_WITH_SECTIONID + TERM,
                                       self.naming, False), EXP_INFO)
    for (header, _, _, _) in pol.nsxv_policies:
      filter_options = header.FilterOptions(_PLATFORM)
      pol._ParseFilterOptions(filter_options)
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['filter_type'], 'inet')
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['section_id'], '1009')
      self.assertIsNone(nsxv.Nsxv._FILTER_OPTIONS_DICT['applied_to'])

  def testParseFilterOptionsCase2(self):
    pol = nsxv.Nsxv(policy.ParsePolicy(HEADER_WITH_SECURITYGROUP + INET6_TERM,
                                       self.naming, False), EXP_INFO)
    for (header, _, _, _) in pol.nsxv_policies:
      filter_options = header.FilterOptions(_PLATFORM)
      pol._ParseFilterOptions(filter_options)
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['filter_type'], 'inet6')
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['section_id'], 0)
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['applied_to'],
                       'securitygroup-Id1')

  def testBadHeaderCase(self):
    pol = policy.ParsePolicy(BAD_HEADER + INET6_TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testBadHeaderCase1(self):
    pol = policy.ParsePolicy(BAD_HEADER_1 + TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testBadHeaderCase2(self):
    pol = policy.ParsePolicy(BAD_HEADER_2 + INET6_TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testBadHeaderCase3(self):
    pol = policy.ParsePolicy(BAD_HEADER_3 + INET6_TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testBadHeaderCase4(self):
    pol = policy.ParsePolicy(BAD_HEADER_4 + INET6_TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testMixedToV4(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            MIXED_TO_V4)
    self.verify_address_type(source_addr, 'Ipv4Address')
    self.verify_address_type(dest_addr, 'Ipv4Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('GOOGLE_DNS'), mock.call('INTERNAL')])

  def testV4ToMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')],
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            V4_TO_MIXED)
    self.verify_address_type(source_addr, 'Ipv4Address')
    self.verify_address_type(dest_addr, 'Ipv4Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('INTERNAL'), mock.call('GOOGLE_DNS')])

  def testMixedToV6(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')],
        [nacaddr.IP('2001:4860:8000::/33')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            MIXED_TO_V6)
    self.verify_address_type(source_addr, 'Ipv6Address')
    self.verify_address_type(dest_addr, 'Ipv6Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('GOOGLE_DNS'), mock.call('SOME_HOST')])

  def testV6ToMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('2001:4860:8000::/33')],
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            V6_TO_MIXED)
    self.verify_address_type(source_addr, 'Ipv6Address')
    self.verify_address_type(dest_addr, 'Ipv6Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST'), mock.call('GOOGLE_DNS')])

  def testMixedToMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')],
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            MIXED_TO_MIXED)
    self.verify_mixed_address_types(source_addr)
    self.verify_mixed_address_types(dest_addr)

    self.naming.GetNetAddr.assert_has_calls([mock.call('GOOGLE_DNS')] * 2)

  def testMixedToAny(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            MIXED_TO_ANY)
    self.verify_mixed_address_types(source_addr)
    self.assertEqual(len(dest_addr), 0)

    self.naming.GetNetAddr.assert_has_calls([mock.call('GOOGLE_DNS')])

  def testAnyToMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            ANY_TO_MIXED)
    self.assertEqual(len(source_addr), 0)
    self.verify_mixed_address_types(dest_addr)

    self.naming.GetNetAddr.assert_has_calls([mock.call('GOOGLE_DNS')])

  def testV4ToV4(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            V4_TO_V4)
    self.verify_address_type(source_addr, 'Ipv4Address')
    self.verify_address_type(dest_addr, 'Ipv4Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])

  def testV6ToV6(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('2001:4860:8000::/33')],
        [nacaddr.IP('2001:4860:8000::/33')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            V6_TO_V6)
    self.verify_address_type(source_addr, 'Ipv6Address')
    self.verify_address_type(dest_addr, 'Ipv6Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST')] * 2)

  def testV4ToV6(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')],
        [nacaddr.IP('2001:4860:8000::/33')]]

    root = self.get_xml_root(MIXED_HEADER + V4_TO_V6)
    rule = root.findall('./rule')
    # No term(rule) will be rendered in this case
    self.assertEqual(len(rule), 0)
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('INTERNAL'), mock.call('SOME_HOST')])

  def testV6ToV4(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('2001:4860:8000::/33')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]

    root = self.get_xml_root(MIXED_HEADER + V6_TO_V4)
    rule = root.findall('./rule')
    # No term(rule) will be rendered in this case
    self.assertEqual(len(rule), 0)
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST'), mock.call('INTERNAL')])

  def get_xml_root(self, data):
    pol = policy.ParsePolicy(data, self.naming, False)
    target = nsxv.Nsxv(pol, EXP_INFO)

    # parse the output and seperate sections and comment
    section_tokens = str(target).split('<section')
    sections = []

    for sec in section_tokens:
      section = sec.replace('name=', '<section name=')
      sections.append(section)
    # parse the xml
    root = ET.fromstring(sections[1])
    return root

  def get_source_dest_addresses(self, data):
    root = self.get_xml_root(data)
    source_addr = root.findall('./rule/sources/source')
    dest_addr = root.findall('./rule/destinations/destination')

    return source_addr, dest_addr

  def verify_address_type(self, address, type_):
    self.assertNotEqual(len(address), 0)
    for addr in address:
      addr_type = addr.find('type').text
      self.assertEqual(addr_type, type_)

  def verify_mixed_address_types(self, address):
    self.assertNotEqual(len(address), 0)
    ipv4_address = False
    ipv6_address = False
    for addr in address:
      addr_type = addr.find('type').text
      if addr_type == 'Ipv4Address':
        ipv4_address = True
      if addr_type == 'Ipv6Address':
        ipv6_address = True

    self.assertTrue(ipv4_address)
    self.assertTrue(ipv6_address)


if __name__ == '__main__':
  absltest.main()


# tests/lib/nsxv_unittest.py

# Copyright 2015 The Capirca Project Authors 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.
#
"""UnitTest class for nsxv.py."""

import optparse
from absl.testing import absltest
from xml.etree import ElementTree as ET

from capirca.lib import naming
from capirca.lib import nsxv
from capirca.lib import policy
from capirca.tests.lib import nsxv_mocktest


class TermTest(absltest.TestCase):

  def setUp(self):
    """Call before every test case."""
    super().setUp()
    parser = optparse.OptionParser()
    parser.add_option(
        '-d',
        '--def',
        dest='definitions',
        help='definitions directory',
        default='../def')
    (FLAGS, args) = parser.parse_args()
    self.defs = naming.Naming(FLAGS.definitions)

  def tearDown(self):
    super().setUp()
    pass

  def runTest(self):
    pass

  def test_init_forinet(self):
    """Test for Term._init_."""
    inet_term = nsxv.Term(nsxv_mocktest.INET_TERM, 'inet')
    self.assertEqual(inet_term.af, 4)
    self.assertEqual(inet_term.filter_type, 'inet')

  def test_init_forinet6(self):
    """Test for Term._init_."""
    inet6_term = nsxv.Term(nsxv_mocktest.INET6_TERM, 'inet6', 6)
    self.assertEqual(inet6_term.af, 6)
    self.assertEqual(inet6_term.filter_type, 'inet6')

  def test_ServiceToStr(self):
    """Test for Term._ServiceToStr."""

    proto = 6
    icmp_types = []
    dports = [(1024, 65535)]
    spots = [(123, 123)]
    nsxv_term = nsxv.Term(nsxv_mocktest.INET_TERM, 'inet')
    service = nsxv_term._ServiceToString(proto, spots, dports, icmp_types)
    self.assertEqual(
        service, '<service><protocol>6</protocol><sourcePort>123'
        '</sourcePort><destinationPort>1024-65535'
        '</destinationPort></service>')

  def test_str_forinet(self):
    """Test for Term._str_."""
    pol = policy.ParsePolicy(nsxv_mocktest.INET_FILTER, self.defs, False)
    af = 4
    for _, terms in pol.filters:
      nsxv_term = nsxv.Term(terms[0], af)
      rule_str = nsxv.Term.__str__(nsxv_term)
    # parse xml rule and check if the values are correct
    root = ET.fromstring(rule_str)
    # check name and action
    self.assertEqual(root.find('name').text, 'allow-ntp-request')
    self.assertEqual(root.find('action').text, 'allow')

    # check source address
    exp_sourceaddr = ['10.0.0.1', '10.0.0.2']
    for destination in root.findall('./sources/source'):
      self.assertEqual((destination.find('type').text), 'Ipv4Address')
      value = (destination.find('value').text)
      if value not in exp_sourceaddr:
        self.fail('IPv4Address source address not found in test_str_forinet()')

    # check destination address
    exp_destaddr = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
    for destination in root.findall('./destinations/destination'):
      self.assertEqual((destination.find('type').text), 'Ipv4Address')
      value = (destination.find('value').text)
      if value not in exp_destaddr:
        self.fail('IPv4Address destination not found in test_str_forinet()')

    # check protocol
    protocol = int(root.find('./services/service/protocol').text)
    self.assertEqual(protocol, 17)

    # check source port
    source_port = root.find('./services/service/sourcePort').text
    self.assertEqual(source_port, '123')

    # check destination port
    destination_port = root.find('./services/service/destinationPort').text
    self.assertEqual(destination_port, '123')

    # check notes
    notes = root.find('notes').text
    self.assertEqual(notes, 'Allow ntp request')

  def test_str_forinet6(self):
    """Test for Term._str_."""
    pol = policy.ParsePolicy(nsxv_mocktest.INET6_FILTER, self.defs, False)
    af = 6
    filter_type = 'inet6'
    for _, terms in pol.filters:
      nsxv_term = nsxv.Term(terms[0], filter_type, af)
      rule_str = nsxv.Term.__str__(nsxv_term)

    # parse xml rule and check if the values are correct
    root = ET.fromstring(rule_str)
    # check name and action
    self.assertEqual(root.find('name').text, 'test-icmpv6')
    self.assertEqual(root.find('action').text, 'allow')

    # check protocol and sub protocol
    exp_subprotocol = [128, 129]
    for service in root.findall('./services/service'):
      protocol = int(service.find('protocol').text)
      self.assertEqual(protocol, 58)

      sub_protocol = int(service.find('subProtocol').text)
      if sub_protocol not in exp_subprotocol:
        self.fail('subProtocol not matched in test_str_forinet6()')

  def test_TranslatePolicy(self):
    """Test for Nsxv.test_TranslatePolicy."""
    # exp_info default is 2
    exp_info = 2
    pol = policy.ParsePolicy(nsxv_mocktest.INET_FILTER, self.defs, False)
    translate_pol = nsxv.Nsxv(pol, exp_info)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'inet')
      self.assertEqual(filter_list, ['inet'])
      self.assertEqual(len(terms), 1)

  def test_nsxv_str(self):
    """Test for Nsxv._str_."""
    # exp_info default is 2
    exp_info = 2
    pol = policy.ParsePolicy(nsxv_mocktest.MIXED_FILTER, self.defs, False)
    target = nsxv.Nsxv(pol, exp_info)

    # parse the xml and check the values
    root = ET.fromstring(str(target))
    # check section name
    section_name = {'id': '1009', 'name': 'MIXED_FILTER_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'accept-to-honestdns')
    self.assertEqual(root.find('./rule/action').text, 'allow')

    # check IPV4 and IPV6 destinations
    exp_ipv4dest = ['8.8.4.4', '8.8.8.8']
    exp_ipv6dest = ['2001:4860:4860::8844', '2001:4860:4860::8888']

    for destination in root.findall('./rule/destinations/destination'):
      obj_type = destination.find('type').text
      value = (destination.find('value').text)

      if 'Ipv4Address' in obj_type:
        if value not in exp_ipv4dest:
          self.fail('IPv4Address not found in test_nsxv_str()')
      else:
        if value not in exp_ipv6dest:
          self.fail('IPv6Address not found in test_nsxv_str()')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 17)

    # check destination port
    destination_port = root.find('./rule/services/service/destinationPort').text
    self.assertEqual(destination_port, '53')

    # check notes
    notes = root.find('./rule/notes').text
    self.assertEqual(notes, 'Allow name resolution using honestdns.')

  if __name__ == '__main__':
    absltest.main()


# tests/lib/openconfig_test.py

# Copyright 2021 Google 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.

"""Unittest for OpenConfig rendering module."""

import json
from absl.testing import absltest
from unittest import mock

from absl.testing import parameterized
from capirca.lib import aclgenerator
from capirca.lib import openconfig
from capirca.lib import gcp
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "The general policy comment."
  target:: openconfig inet
}
"""

GOOD_SADDR = """
term good-term-1 {
  comment:: "Allow source address."
  source-address:: CORP_EXTERNAL
  action:: accept
}
"""

GOOD_DADDR = """
term good-term-1 {
  comment:: "Allow destination address."
  destination-address:: CORP_EXTERNAL
  action:: accept
}
"""

GOOD_SPORT = """
term good-term-1 {
  comment:: "Allow TCP 53 source."
  source-port:: DNS
  protocol:: tcp
  action:: accept
}
"""

GOOD_DPORT = """
term good-term-1 {
  comment:: "Allow TCP 53 dest."
  destination-port:: DNS
  protocol:: tcp
  action:: accept
}
"""

GOOD_MULTI_PROTO_DPORT = """
term good-term-1 {
  comment:: "Allow TCP & UDP 53."
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_EVERYTHING = """
term good-term-1 {
  comment:: "Allow TCP & UDP 53 with saddr/daddr."
  destination-address:: CORP_EXTERNAL
  source-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_JSON_SADDR = """
[
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv4": {
      "config": {
        "source-address": "10.2.3.4/32"
      }
    }
  }
]
"""

GOOD_JSON_V6_SADDR = """
 [
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv6": {
      "config": {
        "source-address": "2001:4860:8000::5/128"
      }
    }
  }
]
"""

GOOD_JSON_DADDR = """
[
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv4": {
      "config": {
        "destination-address": "10.2.3.4/32"
      }
    }
  }
]
"""

GOOD_JSON_V6_DADDR = """
 [
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv6": {
      "config": {
        "destination-address": "2001:4860:8000::5/128"
      }
    }
  }
]
"""

GOOD_JSON_MIXED_DADDR = """
[
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv4": {
      "config": {
        "destination-address": "10.2.3.4/32"
      }
    }
  },
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv6": {
      "config": {
        "destination-address": "2001:4860:8000::5/128"
      }
    }
  }
]
"""

GOOD_JSON_SPORT = """
[
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv4": {
      "config": {
        "protocol": 6
    },
    "transport": {
      "config": {
        "source-port": 53}
      }
    }
  }
]
"""

GOOD_JSON_DPORT = """
[
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv4": {
      "config": {
        "protocol": 6
    },
    "transport": {
      "config": {
        "destination-port": 53}
      }
    }
  }
]
"""

GOOD_JSON_MULTI_PROTO_DPORT = """
[
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv4": {
      "config": {
        "protocol": 17
    },
    "transport": {
      "config": {
        "destination-port": 53}
      }
    }
  },
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv4": {
      "config": {
        "protocol": 6
      },
      "transport": {
        "config": {
          "destination-port": 53}
        }
      }
  }
]
"""

GOOD_JSON_EVERYTHING = """
 [
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv4": {
      "config": {
        "destination-address": "10.2.3.4/32",
        "protocol": 17,
        "source-address": "10.2.3.4/32"
      },
      "transport": {
        "config": {
          "destination-port": 53
        }
      }
    }
  },
  {
    "actions": {
      "forwarding-action": "ACCEPT"
    },
    "ipv4": {
      "config": {
        "destination-address": "10.2.3.4/32",
        "protocol": 6,
        "source-address": "10.2.3.4/32"
      },
      "transport": {
        "config": {
          "destination-port": 53
        }
      }
    }
  }
]
"""
GOOD_HEADER_INET6 = """
header {
  comment:: "The general policy comment."
  target:: openconfig inet6
}
"""

GOOD_HEADER_MIXED = """
header {
  comment:: "The general policy comment."
  target:: openconfig mixed
}
"""

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2

TEST_IPS = [nacaddr.IP('10.2.3.4/32'),
            nacaddr.IP('2001:4860:8000::5/128')]


_TERM_SOURCE_TAGS_LIMIT = 30
_TERM_TARGET_TAGS_LIMIT = 70
_TERM_PORTS_LIMIT = 256


class OpenConfigTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def _StripAclHeaders(self, acl):
    return '\n'.join([line for line in str(acl).split('\n')
                      if not line.lstrip().startswith('#')])

  def testSaddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_SADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_SADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')

  def testDaddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_DADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_DADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')

  def testSport(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_SPORT, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_SPORT)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp')])

  def testDport(self):
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_DPORT, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_DPORT)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp')])

  def testEverything(self):
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_EVERYTHING, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_EVERYTHING)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'udp'),
        mock.call('DNS', 'tcp')])

  def testV6Saddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER_INET6 + GOOD_SADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_V6_SADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')

  def testV6Daddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER_INET6 + GOOD_DADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_V6_DADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')

  def testMixedDaddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER_MIXED + GOOD_DADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_MIXED_DADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')



if __name__ == '__main__':
  absltest.main()


# tests/lib/packetfilter_test.py

# Copyright 2013 Google 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.

"""Unittest for packetfilter rendering module."""

import datetime
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import packetfilter
from capirca.lib import policy


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter mixed
}
"""

GOOD_HEADER_STATELESS = """
header {
  comment:: "this is a stateless test acl"
  target:: packetfilter test-filter mixed nostate
}
"""

GOOD_HEADER_INET4 = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter
}
"""

GOOD_HEADER_INET6 = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter inet6
}
"""

GOOD_HEADER_DIRECTIONAL = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter out mixed
}
"""

GOOD_HEADER_DIRECTIONAL_STATELESS = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter out mixed nostate
}
"""

GOOD_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_ICMP_TYPES = """
term good-term-icmp-types {
  protocol:: icmp
  icmp-type:: echo-reply unreachable time-exceeded
  action:: deny
}
"""

GOOD_TERM_ICMPV6 = """
term good-term-icmpv6 {
  protocol:: icmpv6
  action:: accept
}
"""

GOOD_TERM_ICMPV6_TYPES = """
term good-term-icmpv6-types {
  protocol:: icmpv6
  icmp-type:: echo-reply
  action:: deny
}
"""

BAD_TERM_ICMP = """
term test-icmp {
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

BAD_TERM_ACTION = """
term bad-term-action {
  protocol:: icmp
  action:: reject-with-tcp-rst
}
"""

GOOD_TERM_TCP = """
term good-term-tcp {
  comment:: "Test term 1"
  destination-address:: PROD_NETWORK
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

DENY_TERM_TCP = """
term deny-term-tcp {
  protocol:: tcp
  action:: deny
}
"""

GOOD_TERM_LOG = """
term good-term-log {
  protocol:: tcp
  logging:: true
  action:: accept
}
"""

EXPIRED_TERM = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRED_TERM2 = """
term expired_test2 {
  expiration:: 2015-01-01
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

NEXT_TERM = """
term next {
  action:: next
}
"""

NEXT_LOG_TERM = """
term next-log {
  logging:: true
  action:: next
}
"""

PORTRANGE_TERM = """
term portrange {
  protocol:: tcp
  action:: accept
  destination-port:: HIGH_PORTS
}
"""

FLAGS_TERM = """
term flags {
  protocol:: tcp
  action:: accept
  option:: syn fin
}
"""

INVALID_FLAGS_TERM = """
term invalid-flags {
  protocol:: udp
  action:: accept
  option:: syn fin
}
"""

MULTILINE_COMMENT = """
term multiline-comment {
  comment:: "This is a
multiline comment"
  protocol:: tcp
  action:: accept
}
"""

TCP_STATE_TERM = """
term tcp-established-only {
  protocol:: tcp
  option:: established
  action:: accept
}
"""

TCP_GOOD_ESTABLISHED_TERM = """
term tcp-established-good {
  protocol:: tcp
  option:: established
  action:: accept
}
"""

TCP_BAD_ESTABLISHED_TERM = """
term tcp-established-bad {
  protocol:: tcp
  option:: established syn
  action:: accept
}
"""

UDP_ESTABLISHED_TERM = """
term udp-established {
  protocol:: udp
  option:: established
  action:: accept
}
"""

MULTIPLE_NAME_TERM = """
term multiple-name {
  protocol:: tcp
  destination-address:: PROD_NETWORK
  destination-port:: SMTP
  source-address:: CORP_INTERNAL
  action:: accept
}
"""

LONG_NAME_TERM_DNS_TCP = """
term multiple-name-dns-tcp {
  protocol:: tcp
  destination-address:: PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME
  destination-port:: DNS
  action:: accept
}
"""

LONG_NAME_TERM_DNS_UDP = """
term multiple-name-dns-udp {
  protocol:: udp
  destination-address:: PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME
  destination-port:: DNS
  action:: accept
}
"""

NON_SHORTENED_LONG_NAME_TERM_DNS_UDP = """
term multiple-name-dns-udp {
  protocol:: udp
  destination-address:: PROD_NETWORK_EXTREAMLY_LONG_VER
  destination-port:: DNS
  action:: accept
}
"""

DUPLICATE_DIFFERENT_LONG_NAME_TERM = """
term multiple-name {
  protocol:: tcp
  destination-address:: PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME
  destination-port:: SMTP
  source-address:: PROD_NETWORK_EXTREAMLY_LONG_VERY_GOOD_NAME
  action:: accept
}
"""

BAD_PROTO_TERM = """
term bad-proto {
  protocol:: hopopt
  action:: accept
}
"""

GOOD_WARNING_TERM = """
term good-warning {
  protocol:: tcp
  policer:: batman
  action:: accept
}
"""

SOURCE_INTERFACE_TERM = """
term src-intf {
  source-interface:: lo0
  action:: accept
}
"""

DESTINATION_INTERFACE_TERM = """
term dest-intf {
  destination-interface:: lo0
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_interface',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'source_interface',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'syn',
               'ack',
               'fin',
               'rst',
               'urg',
               'psh',
               'all',
               'established',
               'tcp-established'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class PacketFilterTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testTcp(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-tcp', result,
                  'did not find comment for good-term-tcp')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { <PROD_NETWORK> } port '
        '{ 25 }', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWORK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testLog(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_LOG, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-log', result,
                  'did not find comment for good-term-log')
    self.assertIn(
        'pass quick log proto { tcp } from { any } to { any } flags S/SA '
        'keep state\n', result,
        'did not find actual term for good-term-log')

  def testIcmp(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-icmp', result,
                  'did not find comment for good-term-icmp')
    self.assertIn(
        'pass quick proto { icmp } from { any } to { any } keep state\n',
        result, 'did not find actual term for good-term-icmp')

  def testIcmpTypes(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP_TYPES, self.naming), EXP_INFO)
    acl_v6_header = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_INET6 + GOOD_TERM_ICMP_TYPES, self.naming), EXP_INFO)
    result = str(acl)
    result_v6_header = str(acl_v6_header)
    self.assertIn('# term good-term-icmp-types', result,
                  'did not find comment for good-term-icmp-types')
    self.assertIn('# term good-term-icmp-types', result_v6_header,
                  'did not find comment for good-term-icmp-types'
                  'in V6 header')
    self.assertIn(
        'block drop quick proto { icmp } from { any } to { any } '
        'icmp-type { 0, 3, 11 }', result,
        'did not find actual term for good-term-icmp-types')
    self.assertIn(
        'block drop quick proto { icmp } from { any } to { any } '
        'icmp-type { 0, 3, 11 }', result,
        'did not find actual term for good-term-icmp-types'
        'in the acl with V6 header')

  def testIcmpv6(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMPV6, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-icmpv6', result,
                  'did not find comment for good-term-icmpv6')
    self.assertIn(
        'pass quick proto { ipv6-icmp } from { any } to { any } keep state\n',
        result,
        'did not find actual term for good-term-icmpv6')

  def testIcmpv6Types(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMPV6_TYPES, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-icmpv6-types', result,
                  'did not find comment for good-term-icmpv6-types')
    self.assertIn(
        'block drop quick proto { ipv6-icmp } from { any } to { any } '
        'icmp6-type { 129 }', result,
        'did not find actual term for good-term-icmpv6-types')

  def testBadIcmp(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + BAD_TERM_ICMP, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError, str, acl)

  @mock.patch.object(packetfilter.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and '
        'will not be rendered.', 'expired_test',
        'test-filter')

  @mock.patch.object(packetfilter.logging, 'warning')
  def testExpiredTerm2(self, mock_warn):
    packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRED_TERM2, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and '
        'will not be rendered.', 'expired_test2',
        'test-filter')

  @mock.patch.object(packetfilter.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'),
        self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring',
        'test-filter')

  def testMultiprotocol(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + MULTIPLE_PROTOCOLS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term multi-proto', result,
                  'did not find comment for multi-proto')
    self.assertIn(
        'pass quick proto { tcp udp icmp } from { any } '
        'to { any } keep state\n',
        result,
        'did not find actual term for multi-proto')

  def testNextTerm(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + NEXT_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term next', result,
                  'did not find comment for next')
    self.assertIn(
        'pass from { any } to { any } flags S/SA keep state\n', result,
        'did not find actual term for next-term')

  def testNextLogTerm(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + NEXT_LOG_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term next-log', result,
                  'did not find comment for next-log')
    self.assertIn(
        'pass log from { any } to { any } flags S/SA keep state\n', result,
        'did not find actual term for next-log-term')

  def testPortRange(self):
    self.naming.GetServiceByProto.return_value = ['12345-12354']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + PORTRANGE_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term portrange', result,
                  'did not find comment for portrange')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { any } '
        'port { 12345:12354 }', result,
        'did not find actual term for portrange')

    self.naming.GetServiceByProto.assert_called_once_with(
        'HIGH_PORTS', 'tcp')

  def testFlags(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + FLAGS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term flags', result,
                  'did not find comment for flags')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { any } '
        'flags SF/SF', result,
        'did not find actual term for flags')

  def testInvalidFlags(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + INVALID_FLAGS_TERM, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError, str, acl)

  def testMultilineComment(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + MULTILINE_COMMENT, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term multiline-comment', result,
                  'did not find comment for multiline-comment')
    self.assertIn('# This is a\n# multiline comment', result,
                  'did not find multiline comment for multiline-comment')

  def testStateless(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_STATELESS + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-tcp', result,
                  'did not find comment for good-term-tcp')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { <PROD_NETWORK> } port '
        '{ 25 } no state', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWORK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testInet4(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_INET4 + GOOD_TERM_LOG, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-log', result,
                  'did not find comment for good-term-log')
    self.assertIn(
        'pass quick log inet proto { tcp } from { any } to { any } flags S/SA '
        'keep state\n', result,
        'did not find actual term for good-term-log')

  def testInet6(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_INET6 + GOOD_TERM_LOG, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-log', result,
                  'did not find comment for good-term-log')
    self.assertIn(
        'pass quick log inet6 proto { tcp } from { any } to { any } flags S/SA '
        'keep state\n', result,
        'did not find actual term for good-term-log')

  def testDirectional(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-tcp', result,
                  'did not find comment for good-term-tcp')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to { <PROD_NETWORK> } port '
        '{ 25 }', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWORK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testDirectionalSourceInterface(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL + SOURCE_INTERFACE_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term src-intf', result,
                  'did not find comment for src-intf')
    self.assertIn(
        'pass in quick on lo0 from { any } to { any } flags S/SA keep state',
        result,
        'did not find actual term for src-intf')

  def testDirectionalDestinationInterface(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL + DESTINATION_INTERFACE_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term dest-intf', result,
                  'did not find comment for dest-intf')
    self.assertIn(
        'pass out quick on lo0 from { any } to { any } flags S/SA keep state',
        result,
        'did not find actual term for dest-intf')

  def testMultipleHeader(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_STATELESS + GOOD_TERM_LOG + GOOD_HEADER_INET6
        + GOOD_TERM_ICMP,
        self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'pass quick log proto { tcp } from { any } to { any } no state',
        result,
        'did not find actual term for good-term-log')
    self.assertIn(
        'pass quick inet6 proto { icmp } from { any } to { any } no state',
        result,
        'did not find actual term for good-term-icmp')

  def testDirectionalStateless(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL_STATELESS + GOOD_TERM_TCP, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-tcp', result,
                  'did not find comment for good-term-tcp')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to { <PROD_NETWORK> } port '
        '{ 25 } no state', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWORK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testStatelessEstablished(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_STATELESS + TCP_STATE_TERM, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn('# term tcp-established-only', result,
                  'did not find comment for tcp-established-only')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { any } flags A/A no state',
        result,
        'did not find actual term for tcp-established-only')

  def testBadFlags(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + TCP_BAD_ESTABLISHED_TERM, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError, str, acl)

  # While "UDP stateless established" seems to be a strange combination it
  # actually makes sense:  e.g., the state or nostate header is a global
  # header directive and indicates whether we do matching on established by
  # flags or proper connection tracking, and pf's permissiveness allows things
  # like:
  #   proto { udp, tcp } flags A/A no state'
  # whereby the flags only apply to TCP protocol matches.  However, the
  # following is invalid:
  #   proto { udp } flags A/A no state'
  # check to make sure we don't output the latter for things like:
  #   target:: packetfilter nostate
  #   term foo { protocol:: udp option:: established }
  def testUdpStatelessEstablished(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_STATELESS + UDP_ESTABLISHED_TERM, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn('# term udp-established', result,
                  'did not find comment for udp-established')
    self.assertIn(
        'pass quick proto { udp } from { any } to { any } no state',
        result,
        'did not find actual term for udp-established')

  def testStatefulBlock(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + DENY_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term deny-term-tcp', result,
                  'did not find comment for udp-established')
    self.assertIn(
        'block drop quick proto { tcp } from { any } to { any } flags S/SA',
        result,
        'did not find actual term for deny-term-tcp')

  def testTcpEstablished(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + TCP_GOOD_ESTABLISHED_TERM, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn('# term tcp-established-good', result,
                  'did not find comment for tcp-established-good')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { any } flags A/A keep state',
        result,
        'did not find actual term for udp-established')

  def testTableCreation(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK'
    corp_internal_one = nacaddr.IP('100.96.0.1/11', strict=False)
    corp_internal_one.parent_token = 'CORP_INTERNAL'
    corp_internal_two = nacaddr.IP('172.16.0.0/16')
    corp_internal_two.parent_token = 'CORP_INTERNAL'
    self.naming.GetNetAddr.side_effect = [
        [prod_network],
        [corp_internal_one, corp_internal_two]]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + MULTIPLE_NAME_TERM, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn(
        'table <PROD_NETWORK> {10.0.0.0/8}', result,
        'did not find PROD_NETWORKtable in header')
    self.assertIn(
        'table <CORP_INTERNAL> {100.96.0.0/11,\\\n'
        '172.16.0.0/16}', result,
        'did not find CORP_INTERNAL table in header')
    self.assertIn(
        'pass quick proto { tcp } from { <CORP_INTERNAL> } to '
        '{ <PROD_NETWORK> } port { 25 } flags S/SA keep state', result,
        'did not find actual term for multiple-name')

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK'),
        mock.call('CORP_INTERNAL')])
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testTableNameShortened(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    self.naming.GetNetAddr.return_value = [prod_network]
    self.naming.GetServiceByProto.return_value = ['53']

    acl = packetfilter.PacketFilter(
        policy.ParsePolicy(GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP,
                           self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'table <PROD_NETWORK_EXTREAMLY_LONG_VER> {10.0.0.0/8}', result,
        'did not find shortened name in header.')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } flags S/SA keep state', result,
        'did not find actual term for multiple-name')

    self.naming.GetNetAddr.assert_called_once_with(
        'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME')
    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testTableDuplicateShortNameError(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    prod_network_two = nacaddr.IP('172.0.0.0/8')
    prod_network_two.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_GOOD_NAME'
    self.naming.GetNetAddr.side_effect = [
        [prod_network], [prod_network_two]]
    self.naming.GetServiceByProto.return_value = ['25']

    self.assertRaises(
        packetfilter.DuplicateShortenedTableNameError,
        packetfilter.PacketFilter.__init__,
        packetfilter.PacketFilter.__new__(packetfilter.PacketFilter),
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + DUPLICATE_DIFFERENT_LONG_NAME_TERM,
            self.naming),
        EXP_INFO)
    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_GOOD_NAME')])
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testTableSameLongNameSameFilter(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    self.naming.GetNetAddr.return_value = [prod_network]
    self.naming.GetServiceByProto.return_value = ['53']

    acl = packetfilter.PacketFilter(
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP +
            LONG_NAME_TERM_DNS_UDP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'table <PROD_NETWORK_EXTREAMLY_LONG_VER> {10.0.0.0/8}', result,
        'did not find shortened name in header.')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } flags S/SA keep state', result,
        'did not find actual TCP term for multiple-name')
    self.assertIn(
        'pass out quick proto { udp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } keep state', result,
        'did not find actual UDP for multiple-name')

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME')])
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')])

  def testTableSameLongNameDiffFilter(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    self.naming.GetNetAddr.return_value = [prod_network]
    self.naming.GetServiceByProto.return_value = ['53']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP +
        GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_UDP,
        self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'table <PROD_NETWORK_EXTREAMLY_LONG_VER> {10.0.0.0/8}', result,
        'did not find shortened name in header.')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } flags S/SA keep state', result,
        'did not find actual TCP term for multiple-name')
    self.assertIn(
        'pass out quick proto { udp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } keep state', result,
        'did not find actual UDP for multiple-name')

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME')])
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')])

  def testTableDiffObjectsShortenedAndNonShortened(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    prod_network_two = nacaddr.IP('172.0.0.0/8')
    prod_network_two.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VER'
    self.naming.GetNetAddr.side_effect = [
        [prod_network], [prod_network_two]]
    self.naming.GetServiceByProto.return_value = ['53']

    self.assertRaises(
        packetfilter.DuplicateShortenedTableNameError,
        packetfilter.PacketFilter.__init__,
        packetfilter.PacketFilter.__new__(packetfilter.PacketFilter),
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP +
            NON_SHORTENED_LONG_NAME_TERM_DNS_UDP,
            self.naming),
        EXP_INFO)
    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VER')])
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')])

  def testTableDuplicateShortNameErrorDiffFilter(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    prod_network_two = nacaddr.IP('172.0.0.0/8')
    prod_network_two.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VER'
    self.naming.GetNetAddr.side_effect = [
        [prod_network], [prod_network_two]]
    self.naming.GetServiceByProto.return_value = ['53']

    self.assertRaises(
        packetfilter.DuplicateShortenedTableNameError,
        packetfilter.PacketFilter.__init__,
        packetfilter.PacketFilter.__new__(packetfilter.PacketFilter),
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP +
            GOOD_HEADER_DIRECTIONAL + NON_SHORTENED_LONG_NAME_TERM_DNS_UDP,
            self.naming),
        EXP_INFO)
    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VER')])
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')])

  def testTermNameConflict(self):
    self.assertRaises(
        packetfilter.DuplicateTermError,
        packetfilter.PacketFilter.__init__,
        packetfilter.PacketFilter.__new__(packetfilter.PacketFilter),
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + GOOD_TERM_ICMP + GOOD_TERM_ICMP,
            self.naming),
        EXP_INFO)

  def testBadProtoError(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + BAD_PROTO_TERM, self.naming), EXP_INFO)
    self.assertRaises(packetfilter.UnsupportedProtoError, str, acl)

  def testBuildTokens(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    pol1 = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = packetfilter.PacketFilter(
        policy.ParsePolicy(GOOD_HEADER + GOOD_WARNING_TERM,
                           self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


if __name__ == '__main__':
  absltest.main()


# tests/lib/paloaltofw_test.py

# Copyright 2012 Google 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.
"""Unit test for Palo Alto Firewalls acl rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import paloaltofw
from capirca.lib import policy

GOOD_HEADER_1 = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone trust to-zone untrust
}
"""

GOOD_HEADER_2 = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone all to-zone all
}
"""

GOOD_HEADER_INET6 = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone trust to-zone untrust inet6
}
"""

GOOD_HEADER_MIXED = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone trust to-zone untrust mixed
}
"""

BAD_HEADER_1 = """
header {
  comment:: "This header has two address families"
  target:: paloalto from-zone trust to-zone untrust inet6 mixed
}
"""

GRE_PROTO_TERM = """
term test-gre-protocol {
  comment:: "allow GRE protocol to FOOBAR"
  destination-address:: FOOBAR
  protocol:: gre
  action:: accept
}
"""

AH_PROTO_TERM = """
term test-ah-protocol {
  comment:: "allow AH protocol to FOOBAR"
  destination-address:: FOOBAR
  protocol:: ah
  action:: accept
}
"""

AH_TCP_MIXED_PROTO_TERM = """
term test-mixed-protocol {
  source-address:: FOOBAR
  protocol:: ah tcp
  action:: accept
  comment:: "Applications and Services should be split into separate terms."
}
"""

ESP_PROTO_TERM = """
term test-esp-protocol {
  comment:: "allow ESP protocol to FOOBAR"
  destination-address:: FOOBAR
  protocol:: esp
  action:: accept
}
"""

ESP_TCP_MIXED_PROTO_TERM = """
term test-mixed-protocol {
  destination-address:: FOOBAR
  protocol:: esp tcp
  action:: accept
  comment:: "Applications and Services should be split into separate terms."
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  comment:: "This header is very very very very very very very very very very very very very very very very very very very very large"
  destination-address:: FOOBAR
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}

"""

GOOD_TERM_2 = """
term good-term-4 {
  destination-address:: SOME_HOST
  protocol:: tcp
  pan-application:: ssl http
  action:: accept
}
"""

GOOD_TERM_3 = """
term only-pan-app {
  pan-application:: ssl
  action:: accept
}
"""

GOOD_TERM_4_STATELESS_REPLY = """
term good-term-stateless-reply {
  comment:: "ThisIsAStatelessReply"
  destination-address:: SOME_HOST
  protocol:: tcp
  pan-application:: ssl http
  action:: accept
}
"""

SVC_TERM_1 = """
term ssh-term-1 {
  comment:: "Allow SSH"
  destination-address:: FOOBAR
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}

term smtp-term-1 {
  comment:: "Allow SMTP"
  destination-address:: FOOBAR
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

SVC_TERM_2 = """
term smtp-term-1 {
  comment:: "Allow SMTP"
  destination-address:: FOOBAR
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

TCP_ESTABLISHED_TERM = """
term tcp-established {
  destination-address:: SOME_HOST
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

UDP_ESTABLISHED_TERM = """
term udp-established-term {
  destination-address:: SOME_HOST
  protocol:: udp
  option:: established
  action:: accept
}
"""

UNSUPPORTED_OPTION_TERM = """
term unsupported-option-term {
  destination-address:: SOME_HOST
  protocol:: udp
  option:: inactive
  action:: accept
}
"""

EXPIRED_TERM_1 = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

ICMP_TYPE_TERM_1 = """
term test-icmp {
  protocol:: icmp
  icmp-type:: echo-request echo-reply unreachable
  action:: accept
}
"""

ICMP_TYPE_TERM_2 = """
term test-icmp-2 {
  protocol:: icmp
  icmp-type:: echo-request echo-reply unreachable
  action:: accept
}
"""

ICMPV6_ONLY_TERM = """
term test-icmpv6-only {
  protocol:: icmpv6
  action:: accept
}
"""

ICMPV6_TYPE_TERM = """
term test-icmpv6-types {
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply destination-unreachable
  action:: accept
}
"""

ICMPV6_ABBREVIATION_TYPE_TERM = """
term test-icmpv6-abbreviation-types {
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply destination-unreachable inverse-neighbor-discovery-solicitation inverse-neighbor-discovery-advertisement version-2-multicast-listener-report home-agent-address-discovery-reply
  action:: accept
}
"""

BAD_ICMPV6_TYPE_TERM = """
term test-icmp {
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply unreachable
  action:: accept
  comment:: "This is incorrect because unreachable is not an icmpv6-type."
}
"""

ICMP_ONLY_TERM_1 = """
term test-icmp-only {
  protocol:: icmp
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

DEFAULT_TERM_1 = """
term default-term-1 {
  action:: deny
}
"""

TIMEOUT_TERM = """
term timeout-term {
  protocol:: icmp
  icmp-type:: echo-request
  timeout:: 77
  action:: accept
}
"""

LOGGING_DISABLED = """
term test-disabled-log {
  comment:: "Testing disabling logging for tcp."
  protocol:: tcp
  logging:: disable
  action:: accept
}
"""

LOGGING_BOTH_TERM = """
term test-log-both {
  comment:: "Testing enabling log-both for tcp."
  protocol:: tcp
  logging:: log-both
  action:: accept
}
"""

LOGGING_TRUE_KEYWORD = """
term test-true-log {
  comment:: "Testing enabling logging for udp with true keyword."
  protocol:: udp
  logging:: true
  action:: accept
}
"""

LOGGING_PYTRUE_KEYWORD = """
term test-pytrue-log {
  comment:: "Testing enabling logging for udp with True keyword."
  protocol:: udp
  logging:: True
  action:: accept
}
"""

LOGGING_SYSLOG_KEYWORD = """
term test-syslog-log {
  comment:: "Testing enabling logging for udp with syslog keyword."
  protocol:: udp
  logging:: syslog
  action:: accept
}
"""

LOGGING_LOCAL_KEYWORD = """
term test-local-log {
  comment:: "Testing enabling logging for udp with local keyword."
  protocol:: udp
  logging:: local
  action:: accept
}
"""

ACTION_ACCEPT_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
}
"""

ACTION_COUNT_TERM = """
term test-count-action {
  comment:: "Testing unsupported count action for tcp."
  protocol:: tcp
  action:: count
}
"""

ACTION_NEXT_TERM = """
term test-next-action {
  comment:: "Testing unsupported next action for tcp."
  protocol:: tcp
  action:: next
}
"""

ACTION_DENY_TERM = """
term test-deny-action {
  comment:: "Testing deny action for tcp."
  protocol:: tcp
  action:: deny
}
"""

ACTION_REJECT_TERM = """
term test-reject-action {
  comment:: "Testing reject action for tcp."
  protocol:: tcp
  action:: reject
}
"""

ACTION_RESET_TERM = """
term test-reset-action {
  comment:: "Testing reset action for tcp."
  protocol:: tcp
  action:: reject-with-tcp-rst
}
"""

PLATFORM_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
  platform:: paloalto
}
"""

OTHER_PLATFORM_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
  platform:: juniper
}
"""

PLATFORM_EXCLUDE_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
  platform-exclude:: paloalto
}
"""

OTHER_PLATFORM_EXCLUDE_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
  platform-exclude:: junipersrx
}
"""

HEADER_COMMENTS = """
header {
  comment:: "comment 1"
  comment:: "comment 2"
  target:: paloalto from-zone trust to-zone untrust
}
term policy-1 {
  pan-application:: ssh
  action:: accept
}
term policy-2 {
  pan-application:: web-browsing
  action:: accept
}
header {
  comment:: "comment 3"
  target:: paloalto from-zone trust to-zone dmz
}
term policy-3 {
  pan-application:: web-browsing
  action:: accept
}
header {
  # no comment
  target:: paloalto from-zone trust to-zone dmz-2
}
term policy-4 {
  pan-application:: web-browsing
  action:: accept
}
"""

ZONE_LEN_ERROR = """
header {
  target:: paloalto from-zone %s to-zone %s
}
term policy {
  pan-application:: web-browsing
  action:: accept
}
"""

SUPPORTED_TOKENS = frozenset({
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'logging',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'stateless_reply',
    'timeout',
    'pan_application',
    'translated',
})

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'reject-with-tcp-rst'},
    'option': {'established', 'tcp-established'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2

_IPSET = [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('2001:4860:8000::/33')]
_IPSET2 = [nacaddr.IP('10.23.0.0/22'), nacaddr.IP('10.23.0.6/23', strict=False)]
_IPSET3 = [nacaddr.IP('10.23.0.0/23')]

PATH_VSYS = "./devices/entry[@name='localhost.localdomain']/vsys/entry[@name='vsys1']"
PATH_RULES = PATH_VSYS + '/rulebase/security/rules'
PATH_TAG = PATH_VSYS + '/tag'
PATH_SERVICE = PATH_VSYS + '/service'
PATH_ADDRESSES = PATH_VSYS + '/address'
PATH_ADDRESS_GROUP = PATH_VSYS + '/address-group'


class PaloAltoFWTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testTermAndFilterName(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    paloalto = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1, self.naming), EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='good-term-1']")
    self.assertIsNotNone(x, output)

    self.naming.GetNetAddr.assert_called_once_with('FOOBAR')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testServiceMap(self):
    definitions = naming.Naming()
    definitions._ParseLine('SSH = 22/tcp', 'services')
    definitions._ParseLine('SMTP = 25/tcp', 'services')
    definitions._ParseLine('FOOBAR = 10.0.0.0/8', 'networks')
    definitions._ParseLine('         2001:4860:8000::/33', 'networks')

    pol1 = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + SVC_TERM_1, definitions), EXP_INFO)
    self.assertEqual(
        pol1.service_map.entries, {
            ((), ('22',), 'tcp'): {
                'name': 'service-ssh-term-1-tcp'
            },
            ((), ('25',), 'tcp'): {
                'name': 'service-smtp-term-1-tcp'
            }
        }, pol1.service_map.entries)

    pol2 = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + SVC_TERM_2, definitions), EXP_INFO)
    # The expectation is that there will be a single port mapped.
    self.assertEqual(
        pol2.service_map.entries, {
            ((), ('25',), 'tcp'): {
                'name': 'service-smtp-term-1-tcp'
            }
        }, pol2.service_map.entries)

  def testDefaultDeny(self):
    paloalto = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + DEFAULT_TERM_1, self.naming),
        EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='default-term-1']/action")
    self.assertIsNotNone(x, output)
    self.assertEqual(x.text, 'deny', output)

  def testIcmpTypes(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ICMP_TYPE_TERM_1, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmp']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual(
        ['icmp-echo-reply', 'icmp-echo-request', 'icmp-unreachable'], members,
        output)

  def testIcmpTypesMultiplePolicies(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER_1 + ICMP_TYPE_TERM_1 + GOOD_HEADER_2 + ICMP_TYPE_TERM_2,
        self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmp']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual(
        ['icmp-echo-reply', 'icmp-echo-request', 'icmp-unreachable'], members,
        output)

    # Check second policy as well.
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmp-2']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual(
        ['icmp-echo-reply', 'icmp-echo-request', 'icmp-unreachable'], members,
        output)

  def testIcmpV6Types(self):
    pol = policy.ParsePolicy(GOOD_HEADER_MIXED + ICMPV6_TYPE_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmpv6-types']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual([
        'icmp6-echo-reply', 'icmp6-echo-request',
        'icmp6-destination-unreachable'
    ], members, output)

  def testIcmpV6ApplicationAbbreviation(self):
    pol = policy.ParsePolicy(GOOD_HEADER_MIXED + ICMPV6_ABBREVIATION_TYPE_TERM,
                             self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(
        PATH_RULES +
        "/entry[@name='test-icmpv6-abbreviation-types']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual([
        'icmp6-echo-reply', 'icmp6-echo-request',
        'icmp6-destination-unreachable', 'icmp6-INV-NBR-DSCVR-SOL',
        'icmp6-INV-NBR-DSCVR-ADV', 'icmp6-version-2-MCAST-LSNR-repo',
        'icmp6-home-agent-ADDR-DSCVR-RPL'
    ], members, output)

  def testBadICMP(self):
    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust %s
}
term rule-1 {
%s
  action:: accept
}"""

    T = """
  icmp-type:: echo-request echo-reply
"""

    pol = policy.ParsePolicy(POL % ("", T), self.naming)
    self.assertRaises(paloaltofw.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

    T = """
  protocol:: udp icmp
  icmp-type:: echo-request echo-reply
"""

    pol = policy.ParsePolicy(POL % ("inet", T), self.naming)
    self.assertRaises(paloaltofw.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

    T = """
  protocol:: icmpv6 icmp
  icmp-type:: echo-request
"""

    pol = policy.ParsePolicy(POL % ("mixed", T), self.naming)
    self.assertRaises(paloaltofw.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

    T = """
  protocol:: icmpv6
  icmp-type:: echo echo-reply
"""
    self.assertRaises(policy.TermInvalidIcmpType,
                      policy.ParsePolicy, POL % ("inet6", T), self.naming)

  def testBadICMPv6Type(self):
    pol = policy.ParsePolicy(GOOD_HEADER_MIXED + BAD_ICMPV6_TYPE_TERM,
                             self.naming)
    self.assertRaises(paloaltofw.PaloAltoFWBadIcmpTypeError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testICMPProtocolOnly(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ICMP_ONLY_TERM_1, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmp-only']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertEqual(['icmp'], members, output)

  def testICMPv6ProtocolOnly(self):
    pol = policy.ParsePolicy(GOOD_HEADER_INET6 + ICMPV6_ONLY_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmpv6-only']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertEqual(['ipv6-icmp'], members, output)

  def testSkipStatelessReply(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_4_STATELESS_REPLY,
                             self.naming)

    # Add stateless_reply to terms, there is no current way to include it in the
    # term definition.
    _, terms = pol.filters[0]
    for term in terms:
      term.stateless_reply = True

    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='good-term-stateless-reply']")
    self.assertIsNone(x, output)

  def testSkipEstablished(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + TCP_ESTABLISHED_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='tcp-established']")
    self.assertIsNone(x, output)

    pol = policy.ParsePolicy(GOOD_HEADER_1 + UDP_ESTABLISHED_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='udp-established-term']")
    self.assertIsNone(x, output)

  def testUnsupportedOptions(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + UNSUPPORTED_OPTION_TERM,
                             self.naming)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testBuildTokens(self):
    self.naming.GetServiceByProto.side_effect = [['25'], ['26']]
    pol1 = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testLoggingBoth(self):
    paloalto = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + LOGGING_BOTH_TERM, self.naming),
        EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-log-both']/log-start")
    self.assertEqual(x, 'yes', output)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-log-both']/log-end")
    self.assertEqual(x, 'yes', output)

  def testDisableLogging(self):
    paloalto = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + LOGGING_DISABLED, self.naming),
        EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-disabled-log']/log-start")
    self.assertEqual(x, 'no', output)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-disabled-log']/log-end")
    self.assertEqual(x, 'no', output)

  def testLogging(self):
    for term in [
        LOGGING_SYSLOG_KEYWORD, LOGGING_LOCAL_KEYWORD, LOGGING_PYTRUE_KEYWORD,
        LOGGING_TRUE_KEYWORD
    ]:
      paloalto = paloaltofw.PaloAltoFW(
          policy.ParsePolicy(GOOD_HEADER_1 + term, self.naming), EXP_INFO)
      output = str(paloalto)

      # we don't have term name so match all elements with attribute
      # name at the entry level
      x = paloalto.config.findall(PATH_RULES + '/entry[@name]/log-start')
      self.assertEqual(len(x), 0, output)
      x = paloalto.config.findall(PATH_RULES + '/entry[@name]/log-end')
      self.assertEqual(len(x), 1, output)
      self.assertEqual(x[0].text, 'yes', output)

  def testAcceptAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_ACCEPT_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertEqual(x, 'allow', output)

  def testDenyAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_DENY_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-deny-action']/action")
    self.assertEqual(x, 'deny', output)

  def testRejectAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_REJECT_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-reject-action']/action")
    self.assertEqual(x, 'reset-client', output)

  def testResetAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_RESET_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-reset-action']/action")
    self.assertEqual(x, 'reset-client', output)

  def testCountAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_COUNT_TERM, self.naming)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testNextAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_NEXT_TERM, self.naming)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testPlatformTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + PLATFORM_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertEqual(x, 'allow', output)

  def testOtherPlatformTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + OTHER_PLATFORM_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertIsNone(x, output)

  def testPlatformExcludeTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + PLATFORM_EXCLUDE_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertIsNone(x, output)

  def testOtherPlatformExcludeTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + OTHER_PLATFORM_EXCLUDE_TERM,
                             self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertEqual(x, 'allow', output)

  def testGreProtoTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GRE_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-gre-protocol']/application")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, 'gre', output)

  def testAhProtoTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + AH_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-ah-protocol']/application")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, 'ipsec-ah', output)

  def testAhTcpMixedProtoTerm(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER_1 + AH_TCP_MIXED_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    svc = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-1']/service")
    self.assertIsNotNone(svc, output)
    self.assertEqual(len(svc), 1, output)
    self.assertEqual(svc[0].tag, 'member', output)
    self.assertEqual(svc[0].text, 'any-tcp', output)

    app = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-1']/application")
    self.assertIsNotNone(app, output)
    self.assertEqual(len(app), 1, output)
    self.assertEqual(app[0].tag, 'member', output)
    self.assertEqual(app[0].text, 'any', output)

    # Check second policy as well.
    svc = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-2']/service")
    self.assertIsNotNone(svc, output)
    self.assertEqual(len(svc), 1, output)
    self.assertEqual(svc[0].tag, 'member', output)
    self.assertEqual(svc[0].text, 'application-default', output)

    app = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-2']/application")
    self.assertIsNotNone(app, output)
    self.assertEqual(len(app), 1, output)
    self.assertEqual(app[0].tag, 'member', output)
    self.assertEqual(app[0].text, 'ipsec-ah', output)


  def testEspProtoTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ESP_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-esp-protocol']/application")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, 'ipsec-esp', output)

  def testEspTcpMixedProtoTerm(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER_1 + ESP_TCP_MIXED_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    svc = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-1']/service")
    self.assertIsNotNone(svc, output)
    self.assertEqual(len(svc), 1, output)
    self.assertEqual(svc[0].tag, 'member', output)
    self.assertEqual(svc[0].text, 'any-tcp', output)

    app = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-1']/application")
    self.assertIsNotNone(app, output)
    self.assertEqual(len(app), 1, output)
    self.assertEqual(app[0].tag, 'member', output)
    self.assertEqual(app[0].text, 'any', output)

    # Check second policy as well.
    svc = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-2']/service")
    self.assertIsNotNone(svc, output)
    self.assertEqual(len(svc), 1, output)
    self.assertEqual(svc[0].tag, 'member', output)
    self.assertEqual(svc[0].text, 'application-default', output)

    app = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-2']/application")
    self.assertIsNotNone(app, output)
    self.assertEqual(len(app), 1, output)
    self.assertEqual(app[0].tag, 'member', output)
    self.assertEqual(app[0].text, 'ipsec-esp', output)

  def testHeaderComments(self):
    pol = policy.ParsePolicy(HEADER_COMMENTS, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)

    tag = 'trust_untrust_policy-comment-1'
    x = paloalto.config.find(PATH_TAG + "/entry[@name='%s']/comments" % tag)
    self.assertIsNotNone(x, output)
    self.assertEqual(x.text, 'comment 1 comment 2', output)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='policy-2']/tag")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, tag, output)

    tag = 'trust_dmz_policy-comment-2'
    x = paloalto.config.find(PATH_TAG + "/entry[@name='%s']/comments" % tag)
    self.assertIsNotNone(x, output)
    self.assertEqual(x.text, 'comment 3', output)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='policy-3']/tag")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, tag, output)

    x = paloalto.config.find(PATH_RULES + "/entry[@name='policy-4']/tag")
    self.assertIsNone(x, output)

  def testZoneLen(self):
    ZONE_MAX_LEN = 'Z' * 31
    ZONE_TOO_LONG = 'Z' * 32

    # from
    pol = policy.ParsePolicy(ZONE_LEN_ERROR % (ZONE_MAX_LEN, 'dmz'),
                             self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='policy']/from/member")
    self.assertEqual(x, ZONE_MAX_LEN, output)

    pol = policy.ParsePolicy(ZONE_LEN_ERROR % (ZONE_TOO_LONG, 'dmz'),
                             self.naming)
    self.assertRaisesRegex(paloaltofw.PaloAltoFWNameTooLongError,
                           '^Source zone must be 31 characters max',
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

    # to
    pol = policy.ParsePolicy(ZONE_LEN_ERROR % ('dmz', ZONE_MAX_LEN),
                             self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='policy']/to/member")
    self.assertEqual(x, ZONE_MAX_LEN, output)

    pol = policy.ParsePolicy(ZONE_LEN_ERROR % ('dmz', ZONE_TOO_LONG),
                             self.naming)
    self.assertRaisesRegex(paloaltofw.PaloAltoFWNameTooLongError,
                           '^Destination zone must be 31 characters max',
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def test_ZonesRequired(self):
    BAD_HEADERS = [
        'header{target::paloalto}',
        'header{target::paloalto from-zone x}',
        'header{target::paloalto x x to-zone x}',
    ]

    msg = ('^Palo Alto Firewall filter arguments '
           'must specify from-zone and to-zone[.]$')
    for header in BAD_HEADERS:
      pol = policy.ParsePolicy(header + GOOD_TERM_3, self.naming)
      self.assertRaisesRegex(paloaltofw.UnsupportedFilterError, msg,
                             paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def test_LongComments(self):
    POL = """
header {
  comment:: "%s"
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  comment:: "%s"
  pan-application:: ssl
  action:: accept
}"""

    # get maximum lengths
    pol = policy.ParsePolicy(POL % ('C', 'C'), self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    MAX_TAG_COMMENTS_LENGTH = paloalto._MAX_TAG_COMMENTS_LENGTH
    MAX_RULE_DESCRIPTION_LENGTH = paloalto._MAX_RULE_DESCRIPTION_LENGTH

    tag = 'trust_untrust_policy-comment-1'

    # maximum length
    pol = policy.ParsePolicy(
        POL %
        ('C' * MAX_TAG_COMMENTS_LENGTH, 'C' * MAX_RULE_DESCRIPTION_LENGTH),
        self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)

    x = paloalto.config.findtext(PATH_TAG + "/entry[@name='%s']/comments" % tag)
    self.assertEqual(x, 'C' * MAX_TAG_COMMENTS_LENGTH, output)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/description")
    self.assertEqual(x, 'C' * MAX_RULE_DESCRIPTION_LENGTH, output)

    # maximum length + 1
    pol = policy.ParsePolicy(
        POL % ('C' * (MAX_TAG_COMMENTS_LENGTH + 1), 'C' *
               (MAX_RULE_DESCRIPTION_LENGTH + 1)), self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)

    # verify warning
    with self.assertLogs(level='WARN') as log:
      output = str(paloalto)
      self.assertEqual(len(log.output), 2, log.output)
      self.assertIn('comments exceeds maximum length', log.output[0])
      self.assertIn('description exceeds maximum length', log.output[1])

    x = paloalto.config.findtext(PATH_TAG + "/entry[@name='%s']/comments" % tag)
    self.assertEqual(x, 'C' * MAX_TAG_COMMENTS_LENGTH, output)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/description")
    self.assertEqual(x, 'C' * MAX_RULE_DESCRIPTION_LENGTH, output)

  def testTermLen(self):
    TERM = """
term %s {
  pan-application:: ssl
  action:: accept
}
"""

    # get maximum length
    pol = policy.ParsePolicy(GOOD_HEADER_1 + TERM % 'T', self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    TERM_MAX_LENGTH = paloalto._TERM_MAX_LENGTH

    # maximum length
    term = 'T' * TERM_MAX_LENGTH
    pol = policy.ParsePolicy(GOOD_HEADER_1 + TERM % term, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='%s']" % term)
    self.assertIsNotNone(x, output)

    # maximum length + 1
    term = 'T' * (TERM_MAX_LENGTH + 1)
    pol = policy.ParsePolicy(GOOD_HEADER_1 + TERM % term, self.naming)
    regex = '^Term .+ is too long[.] Limit is %d characters' % TERM_MAX_LENGTH
    self.assertRaisesRegex(aclgenerator.TermNameTooLongError, regex,
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testPanApplication(self):
    POL1 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  action:: accept
}"""

    POL2 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  pan-application:: %s
  action:: accept
  %s
}"""

    APPS = [
        {'app1'},
        {'app1', 'app2'},
        {'app1', 'app2', 'app3'},
    ]

    POL3 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  pan-application:: web-browsing
  action:: accept%s
}"""

    T0 = ''
    T1 = """
  protocol:: tcp
"""
    T2 = """
  protocol:: tcp
  destination-port:: PORT1 PORT2
"""

    POL4 = """
header {
  target:: paloalto from-zone trust to-zone untrust %s
}
term rule-1 {
  pan-application:: web-browsing
  action:: accept
  protocol:: %s
}"""

    POL5 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  pan-application:: web-browsing
  action:: accept
  protocol:: icmp
  icmp-type:: echo-request
}"""

    POL6 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  pan-application:: web-browsing
  protocol:: tcp icmp
  destination-port:: PORT1
  action:: accept
}"""

    pol = policy.ParsePolicy(POL1, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/application/member")
    self.assertEqual(x, 'any', output)

    for i, app in enumerate(APPS):
      pol = policy.ParsePolicy(POL2 % (' '.join(app), ''), self.naming)
      paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
      output = str(paloalto)
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/application/member")
      apps = {elem.text for elem in x}
      self.assertEqual(APPS[i], apps, output)

    for i, app in enumerate(APPS):
      pol = policy.ParsePolicy(POL2 % (' '.join(app), 'protocol:: tcp'),
                               self.naming)
      paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
      output = str(paloalto)
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/application/member")
      apps = {elem.text for elem in x}
      self.assertEqual(APPS[i], apps, output)

    pol = policy.ParsePolicy(POL3 % T0, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/service/member")
    self.assertEqual(x, 'application-default', output)

    pol = policy.ParsePolicy(POL3 % T1, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/service/member")
    self.assertEqual(x, 'any-tcp', output)

    definitions = naming.Naming()
    definitions._ParseLine('PORT1 = 8080/tcp', 'services')
    definitions._ParseLine('PORT2 = 8081/tcp', 'services')
    pol = policy.ParsePolicy(POL3 % T2, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/service/member")
    self.assertEqual(x, 'service-rule-1-tcp', output)

    regex = ('^Term rule-1 contains non tcp, udp protocols '
             'with pan-application:')
    pol = policy.ParsePolicy(POL4 % ('inet', 'tcp icmp'), self.naming)
    self.assertRaisesRegex(paloaltofw.UnsupportedFilterError, regex,
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

    pol = policy.ParsePolicy(POL4 % ('inet6', 'icmpv6'), self.naming)
    self.assertRaisesRegex(paloaltofw.UnsupportedFilterError, regex,
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

    pol = policy.ParsePolicy(POL5, self.naming)
    self.assertRaisesRegex(paloaltofw.UnsupportedFilterError, regex,
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

    self.assertRaisesRegex(policy.MixedPortandNonPortProtos,
                           '^Term rule-1 contains mixed uses of protocols '
                           'with and without port numbers',
                           policy.ParsePolicy, POL6, definitions)

  def testPanPorts(self):
    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
%s
  action:: accept
}"""

    T = """
  protocol:: udp
  destination-port:: NTP
"""

    definitions = naming.Naming()
    definitions._ParseLine('NTP = 123/tcp 123/udp', 'services')
    definitions._ParseLine('DNS = 53/tcp 53/udp', 'services')

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "service-rule-1-udp"
    path = "/entry[@name='%s']/protocol/udp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "123", output)
    path = "/entry[@name='%s']/protocol/udp/source-port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertIsNone(x, output)

    T = """
  protocol:: udp
  source-port:: NTP
"""

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "service-rule-1-udp"
    path = "/entry[@name='%s']/protocol/udp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "0-65535", output)
    path = "/entry[@name='%s']/protocol/udp/source-port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "123", output)

    T = """
  protocol:: tcp
  source-port:: NTP
  destination-port:: NTP DNS
"""

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "service-rule-1-tcp"
    path = "/entry[@name='%s']/protocol/tcp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "53,123", output)
    path = "/entry[@name='%s']/protocol/tcp/source-port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "123", output)

    T = """
  protocol:: tcp
"""

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "any-tcp"
    path = "/entry[@name='%s']/protocol/tcp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "0-65535", output)
    path = "/entry[@name='%s']/protocol/tcp/source-port" % name
    x = paloalto.config.find(PATH_SERVICE + path)
    self.assertIsNone(x, output)

    T = """
  protocol:: tcp udp
"""

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "any-tcp"
    path = "/entry[@name='%s']/protocol/tcp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "0-65535", output)
    name = "any-udp"
    path = "/entry[@name='%s']/protocol/udp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "0-65535", output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/service/member")
    services = {elem.text for elem in x}
    self.assertEqual({"any-tcp", "any-udp"}, services, output)

  def testPortLessNonPort(self):
    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
%s
  action:: accept
}"""

    T = """
  protocol:: udp icmp
"""

    pol = policy.ParsePolicy(POL % T, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1-1']/service/member")
    self.assertTrue(len(x) > 0, output)
    services = {elem.text for elem in x}
    self.assertEqual({"any-udp"}, services, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1-2']/application/member")
    self.assertTrue(len(x) > 0, output)
    applications = {elem.text for elem in x}
    self.assertEqual({"icmp"}, applications, output)

    T = """
  protocol:: udp tcp icmp gre
"""

    pol = policy.ParsePolicy(POL % T, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1-1']/service/member")
    self.assertTrue(len(x) > 0, output)
    services = {elem.text for elem in x}
    self.assertEqual({"any-udp", "any-tcp"}, services, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1-2']/application/member")
    self.assertTrue(len(x) > 0, output)
    applications = {elem.text for elem in x}
    self.assertEqual({"icmp", "gre"}, applications, output)

  def testSrcAnyDstAnyAddressFamily(self):
    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust %s
}
term rule-1 {
  action:: accept
}"""

    pol = policy.ParsePolicy(POL % "mixed", self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    for srcdst in ["source", "destination"]:
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/%s/member" % srcdst)
      self.assertTrue(len(x) == 1, output)
      values = {elem.text for elem in x}
      self.assertEqual({"any"}, values, output)

    pol = policy.ParsePolicy(POL % "inet", self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    for srcdst in ["source", "destination"]:
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/%s/member" % srcdst)
      self.assertTrue(len(x) == 1, output)
      values = {elem.text for elem in x}
      self.assertEqual({"any-ipv4"}, values, output)
      x = paloalto.config.find(PATH_RULES +
                               "/entry[@name='rule-1']/negate-source")
      self.assertIsNone(x, output)
      x = paloalto.config.find(PATH_RULES +
                               "/entry[@name='rule-1']/negate-destination")
      self.assertIsNone(x, output)

    pol = policy.ParsePolicy(POL % "inet6", self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    for srcdst in ["source", "destination"]:
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/%s/member" % srcdst)
      self.assertTrue(len(x) == 1, output)
      values = {elem.text for elem in x}
      self.assertEqual({"any-ipv4"}, values, output)
      x = paloalto.config.find(PATH_RULES +
                               "/entry[@name='rule-1']/negate-source")
      self.assertIsNotNone(x, output)
      x = paloalto.config.find(PATH_RULES +
                               "/entry[@name='rule-1']/negate-destination")
      self.assertIsNotNone(x, output)

  def testNoAddrObj(self):
    definitions = naming.Naming()
    definitions._ParseLine('NET1 = 10.1.0.0/24', 'networks')
    definitions._ParseLine('NET2 = 10.2.0.0/24', 'networks')
    definitions._ParseLine('NET3 = 10.3.1.0/24', 'networks')
    definitions._ParseLine('       10.3.2.0/24', 'networks')
    definitions._ParseLine('NET4 = 2001:db8:0:aa::/64', 'networks')
    definitions._ParseLine('       2001:db8:0:bb::/64', 'networks')
    definitions._ParseLine('NET5 = NET3 NET4', 'networks')
    definitions._ParseLine('NET6 = 4000::/2', 'networks')
    definitions._ParseLine('NET7 = 8000::/1', 'networks')

    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust %s no-addr-obj
}
term rule-1 {
%s
  action:: accept
  protocol:: tcp
}"""

    T = """
  source-address:: NET1
  destination-address:: NET2 NET3
"""

    pol = policy.ParsePolicy(POL % ("inet", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.1.0.0/24"}, addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.2.0.0/24",
                      "10.3.1.0/24", "10.3.2.0/24"}, addrs, output)

    T = """
  source-address:: NET4
"""

    pol = policy.ParsePolicy(POL % ("inet6", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"2001:db8:0:aa::/64", "2001:db8:0:bb::/64"},
                     addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"any"}, addrs, output)

    T = """
  destination-address:: NET5
"""

    pol = policy.ParsePolicy(POL % ("mixed", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"any"}, addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.3.1.0/24", "10.3.2.0/24",
                      "2001:db8:0:aa::/64", "2001:db8:0:bb::/64"},
                     addrs, output)
    T = """
  source-address:: NET6
  destination-address:: NET7
"""
    pol = policy.ParsePolicy(POL % ("mixed", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"4000::/3", "6000::/3"}, addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"8000::/3",
                      "a000::/3", "c000::/3", "e000::/3"}, addrs, output)

    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust inet no-addr-obj
}
term rule-1 {
  source-address:: NET1 NET2
  action:: accept
  protocol:: tcp
}
header {
  target:: paloalto from-zone trust to-zone untrust inet addr-obj
}
term rule-1 {
  destination-address:: NET3
  action:: accept
  protocol:: tcp
}"""

    pol = policy.ParsePolicy(POL, definitions)
    self.assertRaisesRegex(paloaltofw.UnsupportedHeaderError,
                           '^Cannot mix addr-obj and no-addr-obj header '
                           'option in a single policy file$',
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testAddrObj(self):
    definitions = naming.Naming()
    definitions._ParseLine('NET1 = 10.1.0.0/24', 'networks')
    definitions._ParseLine('NET2 = 10.2.0.0/24', 'networks')
    definitions._ParseLine('NET3 = 10.3.1.0/24', 'networks')
    definitions._ParseLine('       10.3.2.0/24', 'networks')
    definitions._ParseLine('NET4 = 4000::/128', 'networks')
    definitions._ParseLine('NET5 = 4000::/2', 'networks')

    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust %s addr-obj
}
term rule-1 {
%s
  action:: accept
  protocol:: tcp
}"""

    T = """
  source-address:: NET1
  destination-address:: NET2 NET3
"""

    pol = policy.ParsePolicy(POL % ("inet", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET1"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET1']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET1_0"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET1_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.1.0.0/24"}, addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET2",
                      "NET3"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET2']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET2_0"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET2_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.2.0.0/24"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET3']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET3_0", "NET3_1"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET3_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.3.1.0/24"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET3_1']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.3.2.0/24"}, addrs, output)

    # These tests check that large IP ranges are broken into equivalent subnets.
    T = """
  source-address:: NET5
"""
    pol = policy.ParsePolicy(POL % ("mixed", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET5"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET5']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET5_0", "NET5_1"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET5_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"4000::/3"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET5_1']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"6000::/3"}, addrs, output)

    T = """
  source-address:: NET4
  destination-address:: NET5
"""
    pol = policy.ParsePolicy(POL % ("mixed", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET5"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET5']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET5_0", "NET5_1"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET5_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"4000::/3"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET5_1']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"6000::/3"}, addrs, output)

if __name__ == '__main__':
  absltest.main()


# tests/lib/pcap_test.py

# Copyright 2016 Google 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.
"""Unittest for pcap rendering module."""

import datetime
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import pcap
from capirca.lib import policy


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: pcap test-filter
}
"""

GOOD_HEADER_IN = """
header {
  comment:: "this is a test acl"
  target:: pcap test-filter in
}
"""

GOOD_HEADER_OUT = """
header {
  comment:: "this is a test acl"
  target:: pcap test-filter out
}
"""

GOOD_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_ICMP_TYPES = """
term good-term-icmp-types {
  protocol:: icmp
  icmp-type:: echo-reply unreachable time-exceeded
  action:: deny
}
"""

GOOD_TERM_ICMPV6 = """
term good-term-icmpv6 {
  protocol:: icmpv6
  action:: accept
}
"""

BAD_TERM_ICMP = """
term test-icmp {
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

BAD_TERM_ACTION = """
term bad-term-action {
  protocol:: icmp
  action:: undefined
}
"""

GOOD_TERM_TCP = """
term good-term-tcp {
  comment:: "Test term 1"
  destination-address:: PROD_NETWRK
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_WARNING_TERM = """
term good-warning-term {
  comment:: "Test term 1"
  destination-address:: PROD_NETWRK
  destination-port:: SMTP
  protocol:: tcp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_LOG = """
term good-term-log {
  protocol:: tcp
  logging:: true
  action:: accept
}
"""
GOOD_ICMP_CODE = """
term good_term {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""
EXPIRED_TERM = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

NEXT_TERM = """
term next {
  action:: next
}
"""

NEXT_LOG_TERM = """
term next-log {
  logging:: true
  action:: next
}
"""

ESTABLISHED_TERM = """
term accept-established {
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

VRRP_TERM = """
term vrrp-term {
  protocol:: vrrp
  action:: accept
}
"""

UNICAST_TERM = """
term unicast-term {
  destination-address:: ANY
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_HBH = """
term good-term-hbh {
  protocol:: hopopt
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'syn',
               'ack',
               'fin',
               'rst',
               'urg',
               'psh',
               'all',
               'none',
               'established',
               'tcp-established'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class PcapFilter(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testTcp(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(dst net 10.0.0.0/8) and (proto \\tcp) and (dst port 25)', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWRK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testLog(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_LOG, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'proto \\tcp', result,
        'did not find actual term for good-term-log')

  def testIcmp(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'proto \\icmp', result,
        'did not find actual term for good-term-icmp')

  def testIcmpCode(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_ICMP_CODE, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('and icmp[icmpcode] == 3', result, result)
    self.assertIn('and icmp[icmpcode] == 4', result, result)

  def testIcmpTypes(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP_TYPES, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(proto \\icmp) and (icmp[icmptype] == 0 or icmp[icmptype] == 3'
        ' or icmp[icmptype] == 11)', result,
        'did not find actual term for good-term-icmp-types')

  def testIcmpv6(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMPV6, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'icmp6', result,
        'did not find actual term for good-term-icmpv6')

  def testBadIcmp(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + BAD_TERM_ICMP, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      str, acl)

  @mock.patch.object(pcap.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and '
        'will not be rendered.', 'expired_test', 'test-filter')

  @mock.patch.object(pcap.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'),
        self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring', 'test-filter')

  def testMultiprotocol(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + MULTIPLE_PROTOCOLS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(proto \\tcp or proto \\udp or proto \\icmp)', result,
        'did not find actual term for multi-proto')

  def testNextTerm(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + NEXT_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('', result,
                  'did not find actual term for good-term-icmpv6')

  def testTcpOptions(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + ESTABLISHED_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(tcp[tcpflags] & (tcp-ack) == (tcp-ack)', result,
        'did not find actual term for established')

  def testVrrpTerm(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + VRRP_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(proto 112)', result,
        'did not find actual term for vrrp')

  def testMultiHeader(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_LOG + GOOD_HEADER + GOOD_TERM_ICMP,
        self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '((((proto \\tcp))\n))\nor\n((((proto \\icmp))\n))', result,
        'did not find actual terms for multi-header')

  def testDirectional(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER_IN + GOOD_TERM_LOG + GOOD_HEADER_OUT + GOOD_TERM_ICMP,
        self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(((dst net localhost and ((proto \\tcp)))\n))\nor\n'
        '(((src net localhost and ((proto \\icmp)))\n))', result,
        'did not find actual terms for directional')

  def testUnicastIPv6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('::/0')]

    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER_IN + UNICAST_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(dst net localhost and ((proto \\tcp)))', result,
        'did not find actual terms for unicast-term')

    self.naming.GetNetAddr.assert_called_once_with('ANY')

  def testHbh(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_HBH, self.naming), EXP_INFO)
    result = str(acl)

    self.assertIn(
        '(ip6 protochain 0)', result,
        'did not find actual terms for unicast-term')

  def testBuildTokens(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']
    pol1 = pcap.PcapFilter(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_TCP,
                                              self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    pol1 = pcap.PcapFilter(
        policy.ParsePolicy(GOOD_HEADER + GOOD_WARNING_TERM,
                           self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


if __name__ == '__main__':
  absltest.main()


# tests/lib/policy_simple_test.py

# Copyright 2016 Google 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.


from absl.testing import absltest

from absl import logging
from capirca.lib import policy_simple


class FieldTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    logging.debug('======> %s <======', self.id())

  def testAppendAppends(self):
    f = policy_simple.Field('Testvalue')
    f.Append('TESTVALUE')
    self.assertEqual(f.value, 'TestvalueTESTVALUE')

  def testStr(self):
    f = policy_simple.Field('Testvalue')
    self.assertEqual('UNKNOWN::Testvalue', str(f))

  def testStrIndents(self):
    f = policy_simple.Field('Testvalue\nTestValue')
    self.assertEqual('UNKNOWN::Testvalue\n            TestValue', str(f))

  def testIntegerField(self):
    self.assertRaises(ValueError, policy_simple.IntegerField, '7.01')
    try:
      _ = policy_simple.IntegerField('7')
    except ValueError:
      self.fail("IntegerField should accept '7' as value.")

  def testNamingFieldRejectsBad(self):
    bads = (
        'corp_internal',
        'CORP+INTERNAL',
    )
    for bad in bads:
      logging.debug('Testing bad "%s".', bad)
      self.assertRaises(ValueError, policy_simple.NamingField, bad)

  def testNamingFieldAcceptsGood(self):
    goods = (
        'CORP_INTERNAL',
        'RFC1918',
        'FOO_BAR102.BAZ101',
    )
    for good in goods:
      try:
        logging.debug('Testing good "%s".', good)
        _ = policy_simple.NamingField(good)
      except ValueError:
        self.fail('Rejected good NamingField value "%s".' % good)

  def testNamingFieldAppendRejectsBad(self):
    f = policy_simple.NamingField('RFC1918')
    bads = (
        'corp_internal',
        'CORP+INTERNAL',
    )
    for bad in bads:
      logging.debug('Testing bad "%s".', bad)
      self.assertRaises(ValueError, f.Append, bad)

  def testNamingFieldAppendAcceptsGood(self):
    f = policy_simple.NamingField('RFC1918')
    goods = (
        'CORP_INTERNAL',
        'RFC1918',
        'FOO_BAR102.BAZ101',
    )
    for good in goods:
      try:
        logging.debug('Testing good "%s".', good)
        _ = f.Append(good)
      except ValueError:
        self.fail('Rejected good NamingField value "%s".' % good)

  def testNamingFieldDedupes(self):
    f = policy_simple.NamingField('RFC1918 CORP_INTERNAL RFC1918')
    f.Append('RFC1918')
    f.Append('CORP_INTERNAL RFC1918')
    self.assertEqual(set(['RFC1918', 'CORP_INTERNAL']), f.value)

  def testNamingFieldStr(self):
    f = policy_simple.NamingField(' '.join(str(x) for x in range(25)))
    expected_str = ('UNKNOWN:: 0 1 10 11 12 13 14 15 16 17 18 19 2 20 21'
                    ' 22 23 24 3 4 5 6 7\n            9')
    self.assertEqual(expected_str, str(f))


class BlockTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    logging.debug('======> %s <======', self.id())

  def testRejectsNonField(self):
    b = policy_simple.Block()
    for t in ('', 3, lambda x: x, policy_simple.Header(),
              policy_simple.Policy('test')):
      self.assertRaises(TypeError, b.AddField, t)

  def testFieldsWithType(self):
    b = policy_simple.Block()
    c1 = policy_simple.Comment('test1')
    c2 = policy_simple.Comment('test2')
    d = policy_simple.DestinationAddress('XYZ')
    s = policy_simple.SourceAddress('ABC')
    for field in (c1, d, c2, s):
      b.AddField(field)

    self.assertEqual([c1, d, c2, s], b.fields)
    self.assertEqual([c1, c2], b.FieldsWithType(policy_simple.Comment))

  def testIter(self):
    a = object()
    b = object()
    c = object()
    block = policy_simple.Block()
    block.fields = (a, b, c)

    self.assertEqual([a, b, c], list(block))


class PolicyTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    logging.debug('======> %s <======', self.id())

  def testAddMember(self):
    p = policy_simple.Policy('test')
    good = [policy_simple.Header(), policy_simple.Term('test'),
            policy_simple.BlankLine(), policy_simple.CommentLine('test'),
            policy_simple.Include('other_pol')]
    bad = ('', 3, lambda x: x, policy_simple.Field('test'))

    for member in good:
      try:
        p.AddMember(member)
      except TypeError:
        self.fail('Policy should accept member "%s"' % member)
    self.assertEqual(good, p.members)

    for member in bad:
      self.assertRaises(TypeError, p.AddMember, member)

  def testIter(self):
    a = object()
    b = object()
    c = object()
    pol = policy_simple.Policy(identifier=None)
    pol.members = (a, b, c)

    self.assertEqual([a, b, c], list(pol))


class PolicyParserTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    logging.debug('======> %s <======', self.id())

  def Parser(self, data):
    return policy_simple.PolicyParser(data=data, identifier='test')

  def testParseCommentLine(self):
    parser = self.Parser('# test-comment-value')
    expected = policy_simple.CommentLine('# test-comment-value')

    pol = parser.Parse()
    self.assertEqual([expected], pol.members)

  def testParseBlankLine(self):
    parser = self.Parser('')
    expected = policy_simple.BlankLine()

    pol = parser.Parse()
    self.assertEqual([expected], pol.members)

  def testParseInclude(self):
    parser = self.Parser('#include other/file #whatever')
    expected = policy_simple.Include('other/file')

    pol = parser.Parse()
    self.assertEqual([expected], pol.members)

  def testParseHeader(self):
    parser = self.Parser('header {\ntarget::Test\n}')
    expected = policy_simple.Header()
    expected.AddField(policy_simple.Target('Test'))

    pol = parser.Parse()
    self.assertEqual(expected, pol.members[0])

  def testParseTerm(self):
    parser = self.Parser('term testy {\ntarget::Test\n}')
    expected = policy_simple.Term('testy')
    expected.AddField(policy_simple.Target('Test'))

    pol = parser.Parse()
    self.assertEqual(expected, pol.members[0])

  def testParseTermBadField(self):
    parser = self.Parser('term testy {\nbad_field::Test\n}')
    self.assertRaises(ValueError, parser.Parse)

  def testUnfinishedBlock(self):
    parser = self.Parser('term testy {\ntarget::Test\n')
    self.assertRaises(ValueError, parser.Parse)


if __name__ == '__main__':
  absltest.main()


# tests/lib/policy_test.py

# Copyright 2011 Google 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.

"""Unit tests for policy.py library."""

from absl.testing import absltest
from unittest import mock

from absl import logging
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy


HEADER = """
header {
  comment:: "this is a test acl"
  comment:: "this is another comment"
  target:: juniper test-filter
}
"""
HEADER_2 = """
header {
  comment:: "this goes in the other direction"
  target:: juniper test-filter-outbound
}
"""
HEADER_3 = """
header {
  comment:: "test header 3"
  target:: cisco 50 standard
}
"""
HEADER_4 = """
header {
  comment:: "test header 4"
  target:: iptables
}
"""
HEADER_5 = """
header {
  comment:: "test header 5"
  target:: gce global/networks/default
}
"""
HEADER_6 = """
header {
  comment:: "this is a test nftable acl"
  target::  nftables chain_name input 0 inet
}
"""
HEADER_V6 = """
header {
  comment:: "this is a test inet6 acl"
  comment:: "this is another comment"
  target:: juniper test-filter inet6
}
"""
HEADER_SRX = """
header {
  target:: srx from-zone foo to-zone bar
}
"""
HEADER_OBJ_GRP = """
header {
  target:: cisco foo object-group
}
"""
HEADER_ADDRBOOK_MIXED = """
header {
  target:: srx from-zone to-zone bar
  target:: cisco foo
}
"""
HEADER_HF_1 = """
header {
  comment:: "This is a test of HF INGRESS Policy."
  target:: gcp_hf INGRESS
}
"""

INCLUDE_STATEMENT = """
#include "/tmp/y.inc"
"""
INCLUDED_Y_FILE = """
term included-term-1 {
  protocol:: tcp
  action:: accept
}
#include "/tmp/z.inc"
"""
GOOD_TERM_0 = """
term good-term-0 {
  protocol:: icmp
  action:: accept
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}
"""
GOOD_TERM_2 = """
term good-term-2 {
  protocol:: tcp
  source-address:: PROD_NETWRK
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: tcp
  source-address:: PROD_NETWRK
  destination-port:: SMTP
  action:: accept
}
"""
GOOD_TERM_4 = """
term good-term-4 {
  protocol:: 1
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  action:: accept
}
"""
GOOD_TERM_6 = """
term good-term-6 {
  protocol:: tcp
  destination-port:: MYSQL HIGH_PORTS
  action:: accept
}
"""
GOOD_TERM_7 = """
term good-term-7 {
  protocol:: tcp
  destination-address:: PROD_NETWRK
  destination-exclude:: PROD_EH
  action:: accept
}
"""
GOOD_TERM_8 = """
term good-term-8 {
  protocol:: tcp udp
  destination-port:: DNS
  action:: accept
}
"""
GOOD_TERM_9 = """
term good-term-9 {
  comment:: "first comment"
  comment:: "second comment"
  action:: accept
}
"""
GOOD_TERM_10 = """
term good-term-10 {
  logging:: true
  action:: accept
}
"""
GOOD_TERM_11 = """
term good-term-11 {
  protocol:: icmp
  icmp-type:: echo-reply echo-request unreachable
  action:: accept
}
"""
GOOD_TERM_12 = """
term qos-good-term-12 {
  action:: accept
  qos:: af4
}
"""
GOOD_TERM_13 = """
term good-term-13 {
  source-port:: GOOGLE_PUBLIC
  source-port:: SNMP
  protocol:: udp
  action:: accept
}
"""
GOOD_TERM_14 = """
term good-term-14 {
  source-prefix:: foo_prefix_list
  action:: accept
}
"""
GOOD_TERM_15 = """
term good-term-15 {
  destination-prefix:: bar_prefix_list baz_prefix_list
  action:: accept
}
"""
GOOD_TERM_16 = """
term good-term-16 {
  ether-type:: arp ipv4
  ether-type:: vlan
  action:: accept
}
"""
GOOD_TERM_17 = """
term good-term-17 {
  traffic-type:: broadcast unknown-unicast
  traffic-type:: multicast
  action:: accept
}
"""
GOOD_TERM_18 = """
term good-term-18 {
  comment:: "test verbatim output"
  verbatim:: iptables "mary had a little lamb"
  verbatim:: juniper "mary had another lamb"
}
"""
GOOD_TERM_19 = """
term good-term-19 {
  source-port:: HTTP MYSQL
  destination-address:: PROD_EXTERNAL_SUPER PROD_NETWRK
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_20 = """
term good-term-20 {
  source-port:: MYSQL HTTP
  destination-address:: PROD_NETWRK PROD_EXTERNAL_SUPER
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_21 = """
term good-term-21 {
  source-port:: MYSQL HTTPS
  destination-address:: PROD_NETWRK PROD_EXTERNAL_SUPER
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_22 = """
term precedence-term {
  protocol:: icmp
  precedence:: 1
  action:: accept
}
"""
GOOD_TERM_23 = """
term loss-priority-term {
  source-port:: SSH
  protocol:: tcp
  loss-priority:: low
  action:: accept
}
"""
GOOD_TERM_24 = """
term routing-instance-term {
  source-port:: SSH
  protocol:: tcp
  routing-instance:: foobar-router
}
"""
GOOD_TERM_25 = """
term source-interface-term {
  source-port:: SSH
  protocol:: tcp
  source-interface:: foo0
  action:: accept
}
"""
GOOD_TERM_26 = """
term good-term-26 {
  protocol:: tcp
  source-address:: PROD_NETWRK
  source-exclude:: PROD_EH
  action:: accept
}
"""
GOOD_TERM_27 = """
term good-term-27 {
  protocol:: tcp
  address:: PROD_NETWRK
  address-exclude:: PROD_EH
  action:: accept
}
"""
GOOD_TERM_28 = """
term good-term-28 {
  protocol:: tcp
  source-address:: PROD_NETWRK
  source-exclude:: BOTTOM_HALF
  action:: accept
}
"""
GOOD_TERM_29 = """
term good-term-29 {
  protocol:: tcp
  option:: tcp-established
  source-address:: PROD_NETWRK
  action:: accept
}
"""
GOOD_TERM_30 = """
term good-term-30 {
  protocol:: tcp
  action:: accept
  vpn:: special-30
}
"""
GOOD_TERM_31 = """
term good-term-31 {
  protocol:: tcp
  action:: accept
  vpn:: special-31 policy-11
}
"""
GOOD_TERM_32 = """
term good-term-32 {
  forwarding-class:: fritzy
  action:: accept
}
"""
GOOD_TERM_33 = """
term good-term-33 {
  forwarding-class:: flashy
  action:: accept
}
"""
GOOD_TERM_34 = """
term good-term-34 {
  source-tag:: src-tag
  destination-tag:: dest-tag
  action:: accept
}
"""
GOOD_TERM_35 = """
term good-term-35 {
  source-address:: PROD_NETWRK
  next-ip:: NEXT_IP
}
"""
GOOD_TERM_36 = """
term good-term-36 {
  forwarding-class:: flashy fritzy
  action:: accept
}
"""
GOOD_TERM_37 = """
term good-term-37 {
  protocol:: icmp
  action:: accept
  log_name:: "my special prefix"
}
"""
GOOD_TERM_38 = """
term good-term-38 {
  source-prefix-except:: foo_prefix_list
  action:: accept
}
"""
GOOD_TERM_39 = """
term good-term-39 {
  destination-prefix-except:: bar_prefix_list baz_prefix_list
  action:: accept
}
"""
GOOD_TERM_40 = """
term good-term-38 {
  source-prefix:: foo_prefix_list
  source-prefix-except:: foo_prefix_list_except
  action:: accept
}
"""
GOOD_TERM_41 = """
term good-term-39 {
  destination-prefix:: bar_prefix_list
  destination-prefix-except:: bar_prefix_list_except
  action:: accept
}
"""
GOOD_TERM_42 = """
term good-term-42 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""
GOOD_TERM_43 = """
term good-term-43 {
  ttl:: 10
  action:: accept
}
"""
GOOD_TERM_44 = """
term good-term-44 {
  logging:: syslog
  log-limit:: 999/day
  action:: accept
}
"""
GOOD_TERM_45 = """
term good-term-45 {
  source-address:: ANY
  action:: accept
  target-service-accounts:: acct1@blah.com
}
"""
GOOD_TERM_46 = """
term good-term-46 {
  protocol:: icmp tcp udp gre esp ah sctp
  encapsulate:: stuff_and_things
}
"""
GOOD_TERM_V6_1 = """
term good-term-v6-1 {
  hop-limit:: 5
  action:: accept
}
"""
GOOD_TERM_V6_2 = """
term good-term-v6-1 {
  hop-limit:: 5-7
  action:: accept
}
"""

TERM_SUPER_2 = """
term term-super {
  address:: PROD
  action:: accept
}
"""
TERM_SUPER_3 = """
term term-super {
  protocol-except:: tcp udp icmpv6
  counter:: stuff_and_things
  action:: reject
}
"""
TERM_SUB_2 = """
term term-sub {
  protocol:: icmp
  action:: accept
}
"""
TERM_UNSORTED_ICMP_TYPE = """
term good-term-11 {
  protocol:: icmp
  icmp-type:: unreachable echo-request echo-reply
  action:: accept
}
"""
TERM_UNSORTED_ICMP_CODE = """
term good-term-11 {
  icmp-type:: unreachable
  icmp-code:: 15 4 9 1
  action:: accept
}
"""
BAD_TERM_1 = """
term bad-term- 1 {
  protocol:: tcp
  action:: reject
}
"""
BAD_TERM_2 = """
term bad-term-2 {
  prootocol:: tcp
  action:: accept
}
"""
BAD_TERM_3 = """
term bad-term-3 {
  protocol:: tcp
  source-port:: SNMP
  action:: accept
}
"""
BAD_TERM_4 = """
term bad-term-4 {
  source-port:: SMTP
  action:: accept
}
"""
BAD_TERM_5 = """
term bad-term-5 {
  protocol:: tcp
  destination-address:: PROD_EH
  destination-exclude:: PROD_NETWRK
  action:: accept
}
"""
BAD_TERM_6 = """
term bad-term-6 {
  logging:: unvalidloggingoption
  action:: accept
}
"""
BAD_TERM_7 = """
term bad-term-7 {
  action:: discard
}
"""
BAD_TERM_8 = """
term bad-term-8 {
  akshun:: accept
}
"""
BAD_TERM_9 = """
term bad-term-9 {
  ether-type:: arp
  protocol:: udp
  action:: accept
}
"""
BAD_TERM_10 = """
term bad-term-10 {
  verbatim:: cisco "mary had a little lamb"

  action:: accept
}
"""
BAD_TERM_12 = """
term bad-term-12 {
  protocol:: icmp
  icmp-type:: echo-foo packet-too-beaucoups
  action:: accept
}
"""
BAD_TERM_13 = """
term bad-term-13 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 99
  action:: accept
}
"""
BAD_TERM_14 = """
term bad-term-14 {
  protocol:: icmp
  icmp-type:: unreachable redirect
  icmp-code:: 3
  action:: accept
}
"""
BAD_TERM_15 = """
term bad-term-15 {
  ttl:: 300
  action:: accept
}
"""
BAD_TERM_16 = """
term bad-term-16 {
  destination-port:: FOO
  protocol:: tcp udp gre
  action:: accept
}
"""

# pylint: disable=maybe-no-member


class PolicyTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  @mock.patch.object(policy, '_ReadFile')
  def testIncludes(self, mock_file):
    """Ensure includes work, as well as nested included."""
    mock_file.side_effect = [INCLUDED_Y_FILE, GOOD_TERM_5]

    # contents of our base policy (which has an included file)
    pol = HEADER + INCLUDE_STATEMENT + GOOD_TERM_1
    p = policy.ParsePolicy(pol, self.naming)
    _, terms = p.filters[0]
    # ensure include worked and we now have 3 terms in this policy
    self.assertEqual(len(terms), 3)
    # ensure included_term_1 is included as first term
    self.assertEqual(terms[0].name, 'included-term-1')
    # ensure good-term-5 is included as second term
    self.assertEqual(terms[1].name, 'good-term-5')
    # ensure good-term-1 shows up as the second term
    self.assertEqual(terms[2].name, 'good-term-1')

    mock_file.assert_has_calls([
        mock.call('/tmp/y.inc'),
        mock.call('/tmp/z.inc')])

  def testGoodPol(self):
    pol = HEADER + GOOD_TERM_1 + GOOD_TERM_2
    self.naming.GetNetAddr.return_value = [nacaddr.IPv4('10.0.0.0/8')]

    ret = policy.ParsePolicy(pol, self.naming)
    # we should only have one filter from that
    self.assertEqual(len(ret.filters), 1)
    header, terms = ret.filters[0]
    self.assertEqual(type(ret), policy.Policy)
    self.assertEqual(str(terms[0].protocol[0]), 'icmp')
    self.assertEqual(len(terms), 2)
    # the comment is stored as a double quoted string, complete with double
    # quotes.
    self.assertEqual(str(header.comment[0]), 'this is a test acl')
    self.assertEqual(str(header.comment[1]), 'this is another comment')
    self.assertEqual(str(header.target[0]), 'juniper')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWRK')

  def testBadPol(self):
    pol = HEADER + BAD_TERM_1
    self.assertRaises(policy.ParseError, policy.ParsePolicy, pol, self.naming)

  def testMissingHeader(self):
    pol = GOOD_TERM_1 + GOOD_TERM_2
    self.assertRaises(policy.ParseError, policy.ParsePolicy, pol, self.naming)

  def testService(self):
    pol = HEADER + GOOD_TERM_1 + GOOD_TERM_3
    self.naming.GetNetAddr.return_value = [nacaddr.IPv4('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(len(terms), 2)
    self.assertEqual(str(terms[1].protocol[0]), 'tcp')
    self.assertEqual(terms[1].destination_port[0], (25, 25))

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWRK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testInvalidKeyword(self):
    pol = HEADER + BAD_TERM_2
    self.assertRaises(policy.ParseError, policy.ParsePolicy, pol, self.naming)

  def testNumericProtocol(self):
    pol = HEADER + GOOD_TERM_4
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].protocol[0]), '1')

  def testHopLimitSingle(self):
    pol = HEADER_V6 + GOOD_TERM_V6_1
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].hop_limit[0]), '5')

  def testHopLimitRange(self):
    pol = HEADER_V6 + GOOD_TERM_V6_2
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].hop_limit[2]), '7')

  def testBadPortProtocols(self):
    pol = HEADER + BAD_TERM_3
    self.naming.GetServiceByProto('SNMP', 'tcp').AndReturn([])
    self.assertRaises(policy.TermPortProtocolError, policy.ParsePolicy, pol,
                      self.naming)

  def testBadPortProtocols2(self):
    pol = HEADER + BAD_TERM_4
    self.assertRaises(policy.TermPortProtocolError, policy.ParsePolicy, pol,
                      self.naming)

  def testMinimumTerm(self):
    pol = HEADER + GOOD_TERM_5
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(len(terms), 1)
    self.assertEqual(str(terms[0].action[0]), 'accept')

  def testPortCollapsing(self):
    pol = HEADER + GOOD_TERM_6
    self.naming.GetServiceByProto.return_value = ['3306']
    self.naming.GetServiceByProto.return_value = ['1024-65535']

    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertSequenceEqual(terms[0].destination_port, [(1024, 65535)])

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('MYSQL', 'tcp'),
        mock.call('HIGH_PORTS', 'tcp')], any_order=True)

  def testPortCollapsing2(self):
    pol = HEADER + GOOD_TERM_8
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertSequenceEqual(terms[0].destination_port, [(53, 53)])

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')], any_order=True)

  def testMinimumTerm2(self):
    pol = HEADER + GOOD_TERM_9
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].comment[0]), 'first comment')
    self.assertEqual(str(terms[0].comment[1]), 'second comment')

  def testLogNameTerm(self):
    pol = HEADER_6 + GOOD_TERM_37
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].log_name), 'my special prefix')

  def testTermEquality(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('64.233.160.0/19'), nacaddr.IPv4('66.102.0.0/20'),
         nacaddr.IPv4('66.249.80.0/20'), nacaddr.IPv4('72.14.192.0/18'),
         nacaddr.IPv4('72.14.224.0/20'), nacaddr.IPv4('216.239.32.0/19')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('64.233.160.0/19'), nacaddr.IPv4('66.102.0.0/20'),
         nacaddr.IPv4('66.249.80.0/20'), nacaddr.IPv4('72.14.192.0/18'),
         nacaddr.IPv4('72.14.224.0/20'), nacaddr.IPv4('216.239.32.0/19')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('64.233.160.0/19'), nacaddr.IPv4('66.102.0.0/20'),
         nacaddr.IPv4('66.249.80.0/20'), nacaddr.IPv4('72.14.192.0/18'),
         nacaddr.IPv4('72.14.224.0/20'), nacaddr.IPv4('216.239.32.0/19')]]
    self.naming.GetServiceByProto.side_effect = [
        ['80'], ['3306'], ['3306'], ['80'], ['3306'], ['443']]

    pol_text = HEADER + GOOD_TERM_19 + GOOD_TERM_20 + GOOD_TERM_21
    ret = policy.ParsePolicy(pol_text, self.naming, shade_check=False)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(len(terms), 3)
    self.assertEqual(terms[0], terms[1])
    self.assertNotEqual(terms[0], terms[2])

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_EXTERNAL_SUPER'),
        mock.call('PROD_NETWRK'),
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EXTERNAL_SUPER'),
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EXTERNAL_SUPER')], any_order=True)
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('HTTP', 'tcp'),
        mock.call('MYSQL', 'tcp'),
        mock.call('MYSQL', 'tcp'),
        mock.call('HTTP', 'tcp'),
        mock.call('MYSQL', 'tcp'),
        mock.call('HTTPS', 'tcp')], any_order=True)

  def testGoodDestAddrExcludes(self):
    pol = HEADER + GOOD_TERM_7
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].destination_address_exclude[0],
                     nacaddr.IPv4('10.62.0.0/15'))

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodSrcAddrExcludes(self):
    pol = HEADER + GOOD_TERM_26
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].source_address_exclude[0],
                     nacaddr.IPv4('10.62.0.0/15'))

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodAddrExcludes(self):
    pol = HEADER + GOOD_TERM_27
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].address_exclude[0],
                     nacaddr.IPv4('10.62.0.0/15'))

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodAddrExcludesFlatten(self):
    expected = sorted([nacaddr.IPv4(u'10.0.0.0/11'),
                       nacaddr.IPv4(u'10.32.0.0/12'),
                       nacaddr.IPv4(u'10.48.0.0/13'),
                       nacaddr.IPv4(u'10.56.0.0/14'),
                       nacaddr.IPv4(u'10.60.0.0/15'),
                       nacaddr.IPv4(u'10.64.0.0/10'),
                       nacaddr.IPv4(u'10.130.0.0/15'),
                       nacaddr.IPv4(u'10.132.0.0/14'),
                       nacaddr.IPv4(u'10.136.0.0/13'),
                       nacaddr.IPv4(u'10.144.0.0/12'),
                       nacaddr.IPv4(u'10.160.0.0/11'),
                       nacaddr.IPv4(u'10.192.0.0/10')])
    pol = HEADER + GOOD_TERM_27
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15'), nacaddr.IPv4('10.129.0.0/15',
                                                    strict=False)]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    terms[0].FlattenAll()

    self.assertEqual(sorted(terms[0].address), expected)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodAddrExcludesFlattenMultiple(self):
    pol = HEADER + GOOD_TERM_27
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.1.0.0/16'),
         nacaddr.IPv4('10.2.0.0/16'),
         nacaddr.IPv4('10.3.0.0/16'),
         nacaddr.IPv4('192.168.0.0/16')],
        [nacaddr.IPv4('10.2.0.0/15')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    terms[0].FlattenAll()
    self.assertEqual(terms[0].address,
                     [nacaddr.IPv4('10.1.0.0/16'),
                      nacaddr.IPv4('192.168.0.0/16')])

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodAddrExcludesFlattenAll(self):
    pol = HEADER + GOOD_TERM_27
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.1.0.0/16'),
         nacaddr.IPv4('10.2.0.0/16'),
         nacaddr.IPv4('10.3.0.0/16')],
        [nacaddr.IPv4('10.0.0.0/8')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    terms[0].FlattenAll()
    self.assertEqual(terms[0].address, [])

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testLogging(self):
    pol = HEADER + GOOD_TERM_10
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].logging[0]), 'true')

  def testBadLogging(self):
    pol = HEADER + BAD_TERM_6
    self.assertRaises(policy.InvalidTermLoggingError, policy.ParsePolicy, pol,
                      self.naming)

  def testBadAction(self):
    pol = HEADER + BAD_TERM_7
    self.assertRaises(policy.InvalidTermActionError, policy.ParsePolicy, pol,
                      self.naming)

  def testMultifilter(self):
    pol = HEADER + GOOD_TERM_1 + HEADER_2 + GOOD_TERM_1
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.headers), 2)

  def testBadMultifilter(self):
    pol = HEADER + HEADER_2 + GOOD_TERM_1
    self.assertRaises(policy.NoTermsError, policy.ParsePolicy, pol,
                      self.naming)

  def testICMPTypes(self):
    pol = HEADER + GOOD_TERM_11
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].icmp_type[0], 'echo-reply')

  def testBadICMPTypes(self):
    pol = HEADER + BAD_TERM_12
    self.assertRaises(policy.TermInvalidIcmpType,
                      policy.ParsePolicy, pol, self.naming)

  def testICMPTypesSorting(self):
    pol = HEADER + TERM_UNSORTED_ICMP_TYPE
    ret = policy.ParsePolicy(pol, self.naming)
    icmp_types = ['echo-reply', 'echo-request', 'unreachable']
    expected = 'icmp_type: %s' % icmp_types
    self.assertIn(expected, str(ret))

  def testICMPCodesSorting(self):
    pol = HEADER + TERM_UNSORTED_ICMP_CODE
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertIn('icmp_code: [1, 4, 9, 15]', str(ret))

  def testReservedWordTermName(self):
    pol = HEADER + GOOD_TERM_12
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].qos, 'af4')
    self.assertEqual(terms[0].name, 'qos-good-term-12')

  def testMultiPortLines(self):
    pol = HEADER + GOOD_TERM_13
    self.naming.GetServiceByProto.side_effect = [['22', '160-162'], ['161']]

    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertSequenceEqual(terms[0].source_port, [(22, 22), (160, 162)])

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('GOOGLE_PUBLIC', 'udp'),
        mock.call('SNMP', 'udp')], any_order=True)

  def testErrorLineNumber(self):
    pol = HEADER + GOOD_TERM_13 + BAD_TERM_8
    self.assertRaisesRegex(policy.ParseError,
                           r'ERROR on "akshun" \(type STRING, line 1',
                           policy.ParsePolicy, pol, self.naming)

  def testPrefixList(self):
    spol = HEADER + GOOD_TERM_14
    dpol = HEADER + GOOD_TERM_15

    # check on the source prefix list
    ret = policy.ParsePolicy(spol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].source_prefix, ['foo_prefix_list'])

    # check on the destination prefix list
    ret = policy.ParsePolicy(dpol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].destination_prefix,
                     ['bar_prefix_list', 'baz_prefix_list'])

  def testPrefixListExcept(self):
    spol = HEADER + GOOD_TERM_38
    dpol = HEADER + GOOD_TERM_39

    # check on the source prefix except list
    ret = policy.ParsePolicy(spol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].source_prefix_except, ['foo_prefix_list'])

    # check on the destination prefix except list
    ret = policy.ParsePolicy(dpol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].destination_prefix_except,
                     ['bar_prefix_list', 'baz_prefix_list'])

  def testPrefixListMixed(self):
    spol = HEADER + GOOD_TERM_40
    dpol = HEADER + GOOD_TERM_41

    # check on the source prefix list with mixed values
    ret = policy.ParsePolicy(spol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].source_prefix, ['foo_prefix_list'])
    self.assertEqual(terms[0].source_prefix_except,
                     ['foo_prefix_list_except'])

    # check on the destination prefix with mixed values
    ret = policy.ParsePolicy(dpol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].destination_prefix, ['bar_prefix_list'])
    self.assertEqual(terms[0].destination_prefix_except,
                     ['bar_prefix_list_except'])

  def testEtherTypes(self):
    pol = HEADER + GOOD_TERM_16
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].ether_type[0], 'arp')
    self.assertEqual(terms[0].ether_type[1], 'ipv4')
    self.assertEqual(terms[0].ether_type[2], 'vlan')

  def testTrafficTypes(self):
    pol = HEADER + GOOD_TERM_17
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].traffic_type[0], 'broadcast')
    self.assertEqual(terms[0].traffic_type[1], 'unknown-unicast')
    self.assertEqual(terms[0].traffic_type[2], 'multicast')

  def testBadProtocolEtherTypes(self):
    pol = HEADER + BAD_TERM_9
    self.assertRaises(policy.TermProtocolEtherTypeError, policy.ParsePolicy,
                      pol, self.naming)

  def testVerbatimTerm(self):
    pol = policy.ParsePolicy(HEADER + GOOD_TERM_18, self.naming)
    _, terms = pol.filters[0]
    self.assertEqual(terms[0].verbatim[0][0], 'iptables')
    self.assertEqual(terms[0].verbatim[0][1], 'mary had a little lamb')
    self.assertEqual(terms[0].verbatim[1][0], 'juniper')
    self.assertEqual(terms[0].verbatim[1][1], 'mary had another lamb')

  def testVerbatimMixed(self):
    pol = HEADER + BAD_TERM_10
    self.assertRaises(policy.ParseError, policy.ParsePolicy, pol, self.naming)

  def testIntegerFilterName(self):
    pol_text = HEADER_3 + GOOD_TERM_0
    pol = policy.ParsePolicy(pol_text, self.naming)
    self.assertEqual(pol.headers[0].target[0].options[0], '50')

  def testPrecedence(self):
    pol_text = HEADER + GOOD_TERM_22
    pol = policy.ParsePolicy(pol_text, self.naming)
    self.assertEqual(len(pol.filters), 1)
    _, terms = pol.filters[0]
    self.assertEqual(terms[0].precedence, [1])

  def testLossPriority(self):
    self.naming.GetServiceByProto.return_value = ['22']

    pol = policy.ParsePolicy(HEADER + GOOD_TERM_23, self.naming)
    self.assertEqual(len(pol.filters), 1)
    _, terms = pol.filters[0]
    self.assertEqual(terms[0].loss_priority, 'low')

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRoutingInstance(self):
    self.naming.GetServiceByProto.return_value = ['22']

    pol = policy.ParsePolicy(HEADER + GOOD_TERM_24, self.naming)
    self.assertEqual(len(pol.filters), 1)
    _, terms = pol.filters[0]
    self.assertEqual(terms[0].routing_instance, 'foobar-router')

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testSourceInterface(self):
    self.naming.GetServiceByProto.return_value = ['22']

    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_25, self.naming)
    self.assertEqual(len(pol.filters), 1)
    header, terms = pol.filters[0]
    self.assertEqual(str(header.target[0]), 'iptables')
    self.assertEqual(terms[0].source_interface, 'foo0')

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testShadingDetection(self):
    pol2 = HEADER + GOOD_TERM_2 + GOOD_TERM_3
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')], [nacaddr.IPv4('10.0.0.0/8')]]
    self.naming.GetServiceByProto.return_value = ['25']

    # same protocol, same saddr, shaded term defines a port.
    self.assertRaises(policy.ShadingError, policy.ParsePolicy, pol2,
                      self.naming, shade_check=True)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_NETWRK')])
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testVpnConfigWithoutPairPolicy(self):
    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_30, self.naming)
    self.assertEqual(len(pol.filters), 1)
    self.assertEqual('special-30', pol.filters[0][1][0].vpn[0])
    self.assertEqual('', pol.filters[0][1][0].vpn[1])

  def testVpnConfigWithPairPolicy(self):
    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_31, self.naming)
    self.assertEqual(len(pol.filters), 1)
    self.assertEqual('special-31', pol.filters[0][1][0].vpn[0])
    self.assertEqual('policy-11', pol.filters[0][1][0].vpn[1])

  def testForwardingClassPolicy(self):
    pol = policy.ParsePolicy(HEADER + GOOD_TERM_32, self.naming)
    self.assertEqual(['fritzy'], pol.filters[0][1][0].forwarding_class)

  def testMultipleForwardingClassPolicy(self):
    pol = policy.ParsePolicy(HEADER + GOOD_TERM_36, self.naming)
    self.assertEqual(['flashy', 'fritzy'],
                     pol.filters[0][1][0].forwarding_class)

  def testForwardingClassEqual(self):
    pol_text = HEADER + GOOD_TERM_32 + GOOD_TERM_33
    ret = policy.ParsePolicy(pol_text, self.naming, shade_check=False)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(len(terms), 2)
    self.assertNotEqual(terms[0], terms[1])

  def testTagSupportAndNetworkHeaderParsing(self):
    pol = policy.ParsePolicy(HEADER_5 + GOOD_TERM_34, self.naming)
    self.assertEqual(len(pol.filters), 1)
    header, terms = pol.filters[0]
    self.assertEqual(str(header.target[0]), 'gce')
    self.assertEqual(header.FilterOptions('gce'), ['global/networks/default'])
    self.assertEqual(terms[0].source_tag, ['src-tag'])
    self.assertEqual(terms[0].destination_tag, ['dest-tag'])

  def testEq(self):
    """Sanity test to verify __eq__ works on Policy objects."""
    policy1 = policy.ParsePolicy(HEADER_4 + GOOD_TERM_30, self.naming)
    policy2 = policy.ParsePolicy(HEADER_4 + GOOD_TERM_30, self.naming)
    policy3 = policy.ParsePolicy(HEADER_5 + GOOD_TERM_34, self.naming)
    self.assertEqual(policy1, policy2)
    self.assertNotEqual(policy1, policy3)
    self.assertNotEqual(policy2, policy3)

  def testNextIP(self):
    pol = HEADER_2 + GOOD_TERM_35
    expected = nacaddr.IPv4('10.1.1.1/32')
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')], [nacaddr.IPv4('10.1.1.1/32')]]

    result = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(result.filters[0][1][0].next_ip[0], expected)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('NEXT_IP')])

  def testStr(self):
    """Sanity test to verify __eq__ works on Policy objects."""
    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_30, self.naming)
    logging.info('Ensuring string formatting doesn\'t throw errors: %s', pol)

  def testTermAddressByteLength(self):
    """Tests the AddressByteLength function."""
    pol = HEADER + GOOD_TERM_2
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv4('10.0.0.1/32'), nacaddr.IPv4('10.0.0.2/32'),
        nacaddr.IPv6('2001:4860:4860::8844/128'),
        nacaddr.IPv6('2001:4860:4860::8888/128')]
    ret = policy.ParsePolicy(pol, self.naming)
    term = ret.filters[0][1][0]
    self.assertEqual(2, term.AddressesByteLength([4]))
    self.assertEqual(8, term.AddressesByteLength([6]))
    self.assertEqual(10, term.AddressesByteLength())

# pylint: enable=maybe-no-member

  def testICMPCodes(self):
    pol = HEADER + GOOD_TERM_42

    result = policy.ParsePolicy(pol, self.naming)
    self.assertIn('icmp_code: [3, 4]', str(result))

  def testBadICMPCodes(self):
    pol = HEADER + BAD_TERM_13
    pol2 = HEADER + BAD_TERM_14
    self.assertRaises(policy.ICMPCodeError, policy.ParsePolicy, pol,
                      self.naming)
    self.assertRaises(policy.ICMPCodeError, policy.ParsePolicy, pol2,
                      self.naming)

  def testOptimizedConsistency(self):
    pol = HEADER + GOOD_TERM_2 + GOOD_TERM_3
    unoptimized_addr = [nacaddr.IPv4('10.16.128.6/32'),
                        nacaddr.IPv4('10.16.128.7/32')]
    optimized_addr = nacaddr.CollapseAddrList(unoptimized_addr)
    self.naming.GetNetAddr.return_value = unoptimized_addr
    self.naming.GetServiceByProto.return_value = ['25']
    ret_unoptimized = policy.ParsePolicy(pol, self.naming, optimize=False)
    self.assertFalse(policy._OPTIMIZE)
    ret_optimized = policy.ParsePolicy(pol, self.naming)
    self.assertTrue(policy._OPTIMIZE)
    for _, terms in ret_unoptimized.filters:
      for term in terms:
        self.assertEqual(unoptimized_addr, term.source_address)
    for _, terms in ret_optimized.filters:
      for term in terms:
        self.assertEqual(optimized_addr, term.source_address)

  def testShadeCheckConsistency(self):
    pol = HEADER + TERM_SUPER_3 + TERM_SUB_2
    self.assertRaises(policy.ShadingError, policy.ParsePolicy, pol, self.naming,
                      shade_check=True)
    self.assertTrue(policy._SHADE_CHECK)
    _ = policy.ParsePolicy(pol, self.naming)
    self.assertFalse(policy._SHADE_CHECK)

  def testEncapsulate(self):
    pol = HEADER + GOOD_TERM_46
    result = policy.ParsePolicy(pol, self.naming)
    self.assertIn('encapsulate: stuff_and_things', str(result))

  def testTTL(self):
    pol = HEADER + GOOD_TERM_43
    result = policy.ParsePolicy(pol, self.naming)
    self.assertIn('ttl: 10', str(result))

  def testInvalidTTL(self):
    pol = HEADER + BAD_TERM_15
    self.assertRaises(policy.InvalidTermTTLValue, policy.ParsePolicy,
                      pol, self.naming)

  def testNeedAddressBook(self):
    pol1 = policy.ParsePolicy(HEADER + GOOD_TERM_1, self.naming)
    pol2 = policy.ParsePolicy(HEADER_SRX + GOOD_TERM_1, self.naming)
    pol3 = policy.ParsePolicy(HEADER_OBJ_GRP + GOOD_TERM_1, self.naming)
    pol4 = policy.ParsePolicy(HEADER_ADDRBOOK_MIXED + GOOD_TERM_1, self.naming)
    self.assertFalse(pol1._NeedsAddressBook())
    self.assertTrue(pol2._NeedsAddressBook())
    self.assertTrue(pol3._NeedsAddressBook())
    self.assertTrue(pol4._NeedsAddressBook())

  def testAddressCleanupCorrect(self):
    unoptimized_addr = [nacaddr.IPv4('10.16.128.6/32', token='FOO'),
                        nacaddr.IPv4('10.16.128.7/32', token='BAR')]
    self.naming.GetNetAddr.return_value = unoptimized_addr
    pol = policy.ParsePolicy(HEADER + GOOD_TERM_2, self.naming)
    term = pol.filters[0][1][0]
    self.assertEqual(nacaddr.CollapseAddrList(unoptimized_addr),
                     term.source_address)
    pol = policy.ParsePolicy(HEADER_SRX + GOOD_TERM_2, self.naming)
    term = pol.filters[0][1][0]
    self.assertEqual(nacaddr.CollapseAddrListPreserveTokens(unoptimized_addr),
                     term.source_address)

  def testLogLimit(self):
    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_44, self.naming)
    term = pol.filters[0][1][0]
    self.assertEqual((u'999', u'day'), term.log_limit)

  def testGREandTCPUDPError(self):
    pol = HEADER + BAD_TERM_16
    self.naming.GetServiceByProto.return_value = ['25']
    self.assertRaises(policy.MixedPortandNonPortProtos, policy.ParsePolicy,
                      pol, self.naming)

  def testTargetServiceAccount(self):
    pol = HEADER_HF_1 + GOOD_TERM_45

    result = policy.ParsePolicy(pol, self.naming)
    term = result.filters[0][1][0]
    self.assertEqual(
        ['acct1@blah.com'],
        term.target_service_accounts)

  # Contains Tests

  def testVerbatimContains(self):
    term_one = policy.Term(policy.VarType(23, ('iptables', 'foo')))
    term_two = policy.Term(policy.VarType(23, ('iptables', 'bar')))
    term_three = policy.Term(policy.VarType(23, ('juniper', 'foo')))
    self.assertIn(term_one, term_one)
    self.assertNotIn(term_two, term_one)
    self.assertNotIn(term_three, term_one)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testIpAndPortContains(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.1.1.1/32')]]
    term_one = policy.Term([policy.VarType(3, 'PROD'),
                            policy.VarType(7, (22, 22)),
                            policy.VarType(7, (80, 80)),
                            policy.VarType(10, 'tcp')])
    term_one.AddObject(policy.VarType(2, 'accept'))
    term_two = policy.Term([policy.VarType(3, 'SMALLER_PROD'),
                            policy.VarType(7, (22, 22)),
                            policy.VarType(10, 'tcp')])
    term_two.AddObject(policy.VarType(2, 'accept'))
    self.assertIn(term_two, term_one)
    self.assertNotIn(term_one, term_two)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testEmptyIpContains(self, mock_naming):
    # testTermContains2 differs from testTermContains in that TERM_SUPER_2
    # only defines a source addres. it's meant to catch the case where
    # the containing term has less detail (and is hence, less restrictive)
    # than the contained term
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.1.1.1/32')]]
    term_one = policy.Term([policy.VarType(5, 'PROD')])
    term_one.AddObject(policy.VarType(2, 'accept'))
    term_two = policy.Term([policy.VarType(3, 'SMALLER_PROD'),
                            policy.VarType(7, (22, 22))])
    term_two.AddObject(policy.VarType(2, 'accept'))
    self.assertIn(term_two, term_one)
    self.assertNotIn(term_one, term_two)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testIpExcludeContains(self, mock_naming):
    # This 'contains' test kicks the tires on source-address and
    # source-address-exclude.
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15')]]
    term_one = policy.Term([policy.VarType(3, 'FOO')])
    term_two = policy.Term([policy.VarType(3, 'FOO'),
                            policy.VarType(11, 'BAR')])
    self.assertIn(term_two, term_one)
    self.assertNotIn(term_one, term_two)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testIpDualExcludeContains(self, mock_naming):
    # One term has (10.0.0.0/8, except 10.10.0.0/24), it should contain a term
    # that has (10.0.0.0/8 except 10.0.0.0/9.
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.10.0.0/24')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.0.0.0/9')]]
    term_one = policy.Term([policy.VarType(3, 'FOO'),
                            policy.VarType(11, 'BAR')])
    term_two = policy.Term([policy.VarType(3, 'FOO'),
                            policy.VarType(11, 'BAR')])
    self.assertIn(term_two, term_one)
    self.assertNotIn(term_one, term_two)

  def testOptionsContains(self):
    # Tests 'contains' testing of the options field. A term without set options
    # contains one which has them set.
    tcp_est_term = policy.Term([policy.VarType(9, 'tcp-established')])
    term = policy.Term([])
    tcp_udp_est_term = policy.Term([policy.VarType(9, 'tcp-established'),
                                    policy.VarType(9, 'established')])
    self.assertNotIn(term, tcp_est_term)
    self.assertNotIn(tcp_est_term, term)
    self.assertIn(tcp_est_term, tcp_udp_est_term)
    self.assertNotIn(tcp_udp_est_term, tcp_est_term)

  def testPrecedenceContains(self):
    # Tests 'contains' testing of the precedence field. A term without set
    # precedence contains one which has them set.
    p_term = policy.Term([policy.VarType(26, 1)])
    no_p_term = policy.Term([])
    self.assertIn(p_term, p_term)
    self.assertIn(no_p_term, no_p_term)
    self.assertNotIn(no_p_term, p_term)
    self.assertNotIn(p_term, no_p_term)

  def testProtocolExceptContains(self):
    # Test the protocol-except keyword.
    pexcept_term = policy.Term([policy.VarType(8, 'tcp')])
    pexpect_term_udp = policy.Term([policy.VarType(8, 'udp')])
    p_term = policy.Term([policy.VarType(10, 'icmp')])
    p_term_tcp = policy.Term([policy.VarType(10, 'tcp')])
    self.assertIn(p_term, pexcept_term)
    self.assertIn(pexcept_term, pexcept_term)
    self.assertNotIn(p_term_tcp, pexcept_term)
    self.assertNotIn(pexpect_term_udp, pexcept_term)

  def testProtocolTermNotInAnotherTermContains(self):
    term_one = policy.Term([policy.VarType(10, 'tcp')])
    term_two = policy.Term([policy.VarType(10, 'udp')])
    self.assertNotIn(term_one, term_two)

  def testTargetServiceAccountContains(self):
    two_target_sa = ['acct1@blah.com', 'acct2@blah.com']
    one_target_sa = ['acct3@blah.com']

    term = policy.Term([policy.VarType(60, two_target_sa)])
    self.assertIn(two_target_sa, term.target_service_accounts)

    term.AddObject(policy.VarType(60, one_target_sa))
    self.assertIn(one_target_sa, term.target_service_accounts)

  def testProtoExceptNotInEmptyTerm(self):
    term_one = policy.Term([policy.VarType(8, 'tcp')])
    term_two = policy.Term([])
    self.assertNotIn(term_two, term_one)

  def testProtocolNotInProtoExcept(self):
    term_one = policy.Term([policy.VarType(8, 'tcp')])
    term_two = policy.Term([policy.VarType(10, 'udp')])
    self.assertNotIn(term_one, term_two)

  def testProtocolNotInEmptyTerm(self):
    term_one = policy.Term([policy.VarType(10, 'tcp')])
    term_two = policy.Term([])
    self.assertNotIn(term_two, term_one)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testAddrNotInAddr(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('192.168.1.1/32')],
        [nacaddr.IPv4('10.1.1.0/24')],
        [nacaddr.IPv4('10.1.1.0/24')],
        [nacaddr.IPv4('10.1.1.0/24')]]
    term = policy.Term([policy.VarType(5, 'FOO')])
    addr_term = policy.Term([policy.VarType(5, 'FOO')])
    saddr_term = policy.Term([policy.VarType(3, 'FOO')])
    daddr_term = policy.Term([policy.VarType(4, 'FOO')])
    self.assertNotIn(addr_term, term)
    self.assertNotIn(saddr_term, term)
    self.assertNotIn(daddr_term, term)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testDestAddrNotInDestAddr(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('192.168.1.1/32')],
        [nacaddr.IPv4('10.1.1.0/24')]]
    term_one = policy.Term([policy.VarType(4, 'FOO')])
    term_two = policy.Term([policy.VarType(4, 'FOO')])
    self.assertNotIn(term_one, term_two)

  def testSourcePortNotInSourcePort(self):
    term_one = policy.Term([policy.VarType(6, (22, 22))])
    term_two = policy.Term([policy.VarType(6, (23, 23))])
    self.assertNotIn(term_one, term_two)

  def testDestinationPortNotInDestinationPort(self):
    term_one = policy.Term([policy.VarType(7, (22, 22))])
    term_two = policy.Term([policy.VarType(7, (23, 23))])
    self.assertNotIn(term_one, term_two)

  def testSourcePrefixContains(self):
    term_one = policy.Term([policy.VarType(19, 'foo')])
    self.assertIn(term_one, term_one)

  def testSourcePrefixNotInSourcePrefix(self):
    term_one = policy.Term([policy.VarType(19, 'foo')])
    term_two = policy.Term([policy.VarType(19, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testDestinationPrefixContains(self):
    term_one = policy.Term([policy.VarType(20, 'foo')])
    self.assertIn(term_one, term_one)

  def testDestinationPrefixNotInDestinationPrefix(self):
    term_one = policy.Term([policy.VarType(20, 'foo')])
    term_two = policy.Term([policy.VarType(20, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testSourcePrefixExceptContains(self):
    term_one = policy.Term([policy.VarType(50, 'foo')])
    self.assertIn(term_one, term_one)

  def testSourcePrefixExceptNotInSourcePrefixExcept(self):
    term_one = policy.Term([policy.VarType(50, 'foo')])
    term_two = policy.Term([policy.VarType(50, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testDestinationPrefixExceptContains(self):
    term_one = policy.Term([policy.VarType(51, 'foo')])
    self.assertIn(term_one, term_one)

  def testDestinationPrefixExceptNotInDestinationPrefixExcept(self):
    term_one = policy.Term([policy.VarType(51, 'foo')])
    term_two = policy.Term([policy.VarType(51, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testSourceTagContains(self):
    term_one = policy.Term([policy.VarType(44, 'foo')])
    self.assertIn(term_one, term_one)

  def testSourceTagNotInSourceTag(self):
    term_one = policy.Term([policy.VarType(44, 'foo')])
    term_two = policy.Term([policy.VarType(44, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testForwardingClassContains(self):
    term_one = policy.Term([policy.VarType(43, 'foo')])
    term_two = policy.Term(
        [policy.VarType(43, 'bar'),
         policy.VarType(43, 'foo')])
    self.assertIn(term_one, term_one)
    self.assertIn(term_one, term_two)

  def testForwardingClassNotIn(self):
    term_one = policy.Term([policy.VarType(43, 'foo')])
    term_two = policy.Term([policy.VarType(43, 'bar')])
    term_three = policy.Term([])
    self.assertNotIn(term_one, term_two)
    self.assertNotIn(term_three, term_one)

  def testForwardingClassExceptContains(self):
    term_one = policy.Term([policy.VarType(52, 'foo')])
    self.assertIn(term_one, term_one)

  def testForwardingClassExceptNotIn(self):
    term_one = policy.Term([policy.VarType(52, 'foo')])
    term_two = policy.Term([policy.VarType(52, 'bar')])
    term_three = policy.Term([])
    self.assertNotIn(term_one, term_two)
    self.assertNotIn(term_three, term_one)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testNextIPContained(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('192.168.1.1/32')]]
    term_one = policy.Term([policy.VarType(46, 'FOO')])
    self.assertIn(term_one, term_one)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testNextIPNotIn(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('192.168.1.1/32')]]
    term_one = policy.Term([policy.VarType(46, 'FOO')])
    term_two = policy.Term([])
    self.assertNotIn(term_two, term_one)

  def testPortContains(self):
    # Test 'contains' against port field and that it matches
    # source/destination/port fields.
    port_term = policy.Term([policy.VarType(32, (25, 25))])
    sport_term = policy.Term([policy.VarType(6, (25, 25))])
    dport_term = policy.Term([policy.VarType(7, (25, 25))])
    self.assertIn(sport_term, port_term)
    self.assertIn(dport_term, port_term)
    self.assertIn(port_term, port_term)
    alt_port_term = policy.Term([policy.VarType(32, (25, 30))])
    sport_term = policy.Term([policy.VarType(6, (25, 30))])
    dport_term = policy.Term([policy.VarType(7, (25, 30))])
    self.assertNotIn(alt_port_term, port_term)
    self.assertNotIn(sport_term, port_term)
    self.assertNotIn(dport_term, port_term)

  def testFragmentOffset(self):
    fo_term = policy.Term([])
    fo_term.AddObject(policy.VarType(17, '80'))
    fo_range_term = policy.Term([])
    fo_range_term.AddObject(policy.VarType(17, '60-90'))
    fo_smaller_range_term = policy.Term([])
    fo_smaller_range_term.AddObject(policy.VarType(17, '65-82'))
    term = policy.Term([])

    self.assertIn(fo_term, fo_term)
    self.assertIn(fo_term, fo_range_term)
    self.assertNotIn(fo_range_term, fo_term)
    self.assertIn(fo_smaller_range_term, fo_range_term)
    self.assertNotIn(fo_range_term, fo_smaller_range_term)
    self.assertNotIn(term, fo_term)

  def testTermTargetResources(self):
    target_resources = [('p1', 'v1'), ('p2', 'v2')]
    target_resource_2 = [('p3', 'v3')]
    term_one = policy.Term(
        [policy.VarType(policy.VarType.TARGET_RESOURCES, target_resources)])
    term_one.AddObject(policy.VarType(59, target_resource_2))

    self.assertIn(target_resources, term_one.target_resources)
    self.assertIn(target_resource_2, term_one.target_resources)

  def testParsePolicySingleTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: (proj1,vpc1)
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    self.assertIn(('proj1', 'vpc1'), terms[0].target_resources)

  def testParsePolicyMultipleTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: (proj1,vpc1)
      target-resources:: (proj2,vpc2)
      target-resources:: (proj3,vpc3)
      target-resources:: (proj4,vpc4)
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)

  def testParsePolicyMultipleCommaSepTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: (proj1,vpc1),(proj2,vpc2),(proj3,vpc3),(proj4,vpc4)
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)

  def testParsePolicyMultipleSpaceSepTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: (proj1,vpc1) (proj2,vpc2) (proj3,vpc3) (proj4,vpc4)
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)

  def testParsePolicyMultipleArrayCommaTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: [(proj1,vpc1),(proj2,vpc2),(proj3,vpc3),(proj4,vpc4)]
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)

  def testParsePolicyMultipleArraySpaceTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: [(proj1,vpc1) (proj2,vpc2) (proj3,vpc3) (proj4,vpc4)]
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)


if __name__ == '__main__':
  absltest.main()


# tests/lib/speedway_test.py

# Copyright 2008 Google 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.

"""Unittest for Speedway rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import speedway


GOOD_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: speedway INPUT ACCEPT
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  protocol:: icmp
  policer:: batman
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_interface',
    'destination_port',
    'destination_prefix',
    'expiration',
    'fragment_offset',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'log_limit',
    'name',
    'option',
    'owner',
    'packet_length',
    'platform',
    'platform_exclude',
    'protocol',
    'routing_instance',
    'source_address',
    'source_address_exclude',
    'source_interface',
    'source_port',
    'source_prefix',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'first-fragment',
               'initial',
               'sample',
               'tcp-established',
               'tcp-initial',
               'syn',
               'ack',
               'fin',
               'rst',
               'urg',
               'psh',
               'all',
               'none'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class SpeedwayTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testSpeedwayOutputFormat(self):
    acl = speedway.Speedway(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                               self.naming), EXP_INFO)
    result = []
    result.extend(str(acl).split('\n'))
    self.assertEqual('*filter', result[0],
                     '*filter designation does not appear at top of generated '
                     'policy.')
    self.assertIn(':INPUT ACCEPT', result,
                  'input default policy of accept not set.')
    self.assertIn('-N I_good-term-1', result,
                  'did not find new chain for good-term-1.')
    self.assertIn(
        '-A I_good-term-1 -p icmp -m state --state NEW,ESTABLISHED,RELATED'
        ' -j ACCEPT', result, 'did not find append for good-term-1.')
    self.assertEqual('COMMIT', result[len(result)-2],
                     'COMMIT does not appear at end of output policy.')

  def testBuildTokens(self):
    pol1 = speedway.Speedway(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                                self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = speedway.Speedway(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                                self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


if __name__ == '__main__':
  absltest.main()


# tests/lib/srxlo_test.py

# Copyright 2014 Google 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.

"""Unittest for Srxlo rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import srxlo


GOOD_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: srxlo test-filter inet6
}
"""
GOOD_HEADER_2 = """
header {
  comment:: "this is a test acl"
  target:: srxlo test-filter inet
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmpv6
  action:: accept
}
"""
GOOD_TERM_2 = """
term good-term-2 {
  protocol:: icmpv6
  icmp-type:: destination-unreachable
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: icmpv6
  action:: accept
  option:: inactive
}
"""
GOOD_TERM_4 = """
term good-term-4 {
  protocol:: icmp
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  protocol-except:: icmp
  action:: accept
}
"""
GOOD_TERM_6 = """
term good-term-6 {
  protocol-except:: icmpv6
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'address',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_prefix',
    'destination_prefix_except',
    'dscp_except',
    'dscp_match',
    'dscp_set',
    'ether_type',
    'expiration',
    'filter_term',
    'forwarding_class',
    'forwarding_class_except',
    'fragment_offset',
    'hop_limit',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'loss_priority',
    'name',
    'next_ip',
    'option',
    'owner',
    'packet_length',
    'platform',
    'platform_exclude',
    'policer',
    'port',
    'precedence',
    'protocol',
    'protocol_except',
    'qos',
    'restrict_address_family',
    'routing_instance',
    'source_address',
    'source_address_exclude',
    'source_port',
    'source_prefix',
    'source_prefix_except',
    'traffic_class_count',
    'traffic_type',
    'translated',
    'ttl',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'first-fragment',
               'inactive',
               'is-fragment',
               '.*',  # not actually a lex token!
               'sample',
               'tcp-established',
               'tcp-initial'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class SRXloTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testIcmp(self):
    output = str(srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_1 +
                                                GOOD_TERM_4, self.naming),
                 EXP_INFO))
    self.assertIn('protocol icmp;', output,
                  'missing or incorrect ICMP specification')
    self.assertNotIn('icmp6;', output,
                     'missing or incorrect ICMP specification')
    self.assertNotIn('icmpv6;', output,
                     'missing or incorrect ICMP specification')

  def testIcmpv6(self):
    output = str(srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1 +
                                                GOOD_TERM_4, self.naming),
                 EXP_INFO))
    self.assertIn('next-header icmp6;', output,
                  'missing or incorrect ICMPv6 specification')
    self.assertNotIn('icmp;', output,
                     'missing or incorrect ICMPv6 specification')

  def testIcmpv6Type(self):
    output = str(srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                                self.naming), EXP_INFO))
    self.assertIn('next-header icmp6;', output,
                  'missing or incorrect ICMPv6 specification')
    self.assertIn('icmp-type 1;', output,
                  'missing or incorrect ICMPv6 type specification')

  def testBuildTokens(self):
    # self.naming.GetServiceByProto.side_effect = [['25'], ['26']]
    pol1 = srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                          self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.max_diff = None
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = srxlo.SRXlo(policy.ParsePolicy(
        GOOD_HEADER_1 + GOOD_TERM_1, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testInactiveTerm(self):
    output = str(srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_3,
                                                self.naming), EXP_INFO))
    self.assertIn('inactive: term good-term-3 {', output)

  def testIcmpExcept(self):
    output = str(
        srxlo.SRXlo(
            policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_5, self.naming),
            EXP_INFO))
    self.assertIn('protocol-except icmp;', output,
                  'missing or incorrect ICMP specification in protocol-except')
    self.assertNotIn(
        'icmp6;', output,
        'missing or incorrect ICMP specification in protocol-except')
    self.assertNotIn(
        'icmpv6;', output,
        'missing or incorrect ICMP specification in protocol-except')

  def testIcmpv6Except(self):
    output = str(
        srxlo.SRXlo(
            policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_6, self.naming),
            EXP_INFO))
    self.assertIn(
        'next-header-except icmp6;', output,
        'missing or incorrect ICMPv6 specification in protocol-except')
    self.assertNotIn(
        'icmp;', output,
        'missing or incorrect ICMPv6 specification in protocol-except')


if __name__ == '__main__':
  absltest.main()


# tests/lib/summarizer_test.py

# Copyright 2014 Google 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.

"""Tests for discontinuous subnet mask summarizer."""

import os

import random
import time
from absl.testing import absltest

from absl import logging
from capirca.lib import nacaddr
from capirca.lib import summarizer


class SummarizerTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    random_seed = int(time.time())
    value = os.environ.get('TEST_RANDOM_SEED', '')
    try:
      random_seed = int(value)
    except ValueError:
      pass
    logging.info('Seeding random generator with seed %d', random_seed)
    random.seed(random_seed)

  def testToDottedQuad(self):
    net = summarizer.DSMNet(1 << 32, 4294967264)
    self.assertRaises(ValueError)
    net = summarizer.DSMNet(3232235584, 1 << 16)
    self.assertRaises(ValueError)
    net = summarizer.DSMNet(3232235584, 4294967264)
    self.assertEqual(summarizer.ToDottedQuad(net),
                     ('192.168.0.64', '255.255.255.224'))
    net = summarizer.DSMNet(3232235584, 4294901984)
    self.assertEqual(summarizer.ToDottedQuad(net, negate=True),
                     ('192.168.0.64', '0.0.255.31'))

    test_data = [(summarizer.DSMNet(3232235584, 4294967295), True,
                  ('192.168.0.64', '32')),
                 (summarizer.DSMNet(3232235584, 4294901760), True,
                  ('192.168.0.64', '16')),
                 (summarizer.DSMNet(3232235584, 4294967294), True,
                  ('192.168.0.64', '31')),
                 (summarizer.DSMNet(3232235584, 4290772992), True,
                  ('192.168.0.64', '10')),
                 (summarizer.DSMNet(3232235584, 4294966016), True,
                  ('192.168.0.64', '255.255.251.0')),
                 (summarizer.DSMNet(3232235584, 4294901504), True,
                  ('192.168.0.64', '255.254.255.0'))]

    for net, nondsm, expected in test_data:
      self.assertEqual(summarizer.ToDottedQuad(net, nondsm=nondsm), expected)

  def testInt32ToDottedQuad(self):
    self.assertEqual(summarizer._Int32ToDottedQuad(3232235584),
                     '192.168.0.64')

  def testSummarizeEmptyList(self):
    nets = []
    result = summarizer.Summarize(nets)
    self.assertEqual(result, [])

  def testSummarizeNoNetworks(self):
    nets = []
    for octet in range(0, 256):
      net = nacaddr.IPv4('192.' + str(255 - octet) + '.' +
                         str(octet) + '.64/27')
      nets.append(net)
    random.shuffle(nets)
    result = summarizer.Summarize(nets)
    self.assertEqual(len(result), 256)

  def testSummarizeSomeNetworks(self):
    nets = [
        # continiously summarizable to one /25
        nacaddr.IPv4('192.168.0.0/27'),
        nacaddr.IPv4('192.168.0.32/27'),
        nacaddr.IPv4('192.168.0.64/27'),
        nacaddr.IPv4('192.168.0.96/27'),
        # discontiniously summarizable with above
        nacaddr.IPv4('128.168.0.0/25'),
        # not summarizable with above
        nacaddr.IPv4('10.0.0.0/8'),
    ]
    for octet in range(0, 256):
      net = nacaddr.IPv4('172.16.' + str(octet) + '.96/30')
      nets.append(net)
    random.shuffle(nets)
    result = summarizer.Summarize(nets)
    self.assertEqual(result, [summarizer.DSMNet(167772160, 4278190080),
                              summarizer.DSMNet(2158493696, 3221225344),
                              summarizer.DSMNet(2886729824, 4294902012)])

  def testSummarizeAllNetworks(self):
    nets = []
    for octet in range(0, 256):
      net = nacaddr.IPv4('192.168.' + str(octet) + '.64/27')
      nets.append(net)
    random.shuffle(nets)
    result = summarizer.Summarize(nets)
    # summarizes to 192.168.0.64 / 255.255.0.224
    self.assertEqual(result, [summarizer.DSMNet(3232235584, 4294901984)])

  def testSummarizeToAllSpace(self):
    nets = [
        nacaddr.IPv4('0.0.0.0/1'),
        nacaddr.IPv4('128.0.0.0/1'),
    ]
    random.shuffle(nets)
    result = summarizer.Summarize(nets)
    self.assertEqual(result, [summarizer.DSMNet(0, 0)])

  def testNacaddrNetToDSMNet(self):
    nacaddr_net = nacaddr.IPv4('192.168.0.64/27')
    dsm_net = summarizer.DSMNet(3232235584, 4294967264, '')
    self.assertEqual(summarizer._NacaddrNetToDSMNet(nacaddr_net), dsm_net)

  def testToPrettyBinaryFormat(self):
    # 192.168.0.64
    self.assertEqual(summarizer._ToPrettyBinaryFormat(3232235584),
                     '11000000 10101000 00000000 01000000')
    # 8.8.8.8
    self.assertEqual(summarizer._ToPrettyBinaryFormat(134744072),
                     '00001000 00001000 00001000 00001000')
    # 0.0.0.0
    self.assertEqual(summarizer._ToPrettyBinaryFormat(0),
                     '00000000 00000000 00000000 00000000')
    # fc00::1
    self.assertEqual(
        summarizer._ToPrettyBinaryFormat(
            334965454937798799971759379190646833153),
        '11111100 00000000 00000000 00000000 00000000 00000000 00000000 '
        '00000000 00000000 00000000 00000000 00000000 00000000 00000000 '
        '00000000 00000001')

  def testSummarizeDSMONetworks(self):
    fourth_octet = [2, 8, 20, 26, 28, 32, 40, 52, 58, 86, 130, 136, 148,
                    154, 156, 160, 168, 180, 186, 214]
    nets = list()

    for octet3 in range(56, 60):
      for octet4 in fourth_octet:
        nets.append(nacaddr.IPv4('192.168.' + str(octet3) + '.'
                                 + str(octet4) + '/31'))

    result = summarizer.Summarize(nets)
    self.assertEqual(result, [summarizer.DSMNet(3232249858, 4294966398),
                              summarizer.DSMNet(3232249864, 4294966366),
                              summarizer.DSMNet(3232249876, 4294966390),
                              summarizer.DSMNet(3232249882, 4294966366),
                              summarizer.DSMNet(3232249888, 4294966398),
                              summarizer.DSMNet(3232249908, 4294966398),
                              summarizer.DSMNet(3232249942, 4294966398),
                              ])

  def testMergeText(self):
    existing_comment = 'comment that already exists'
    addition = 'addition'

    dsm_net = summarizer.DSMNet(167772160, 4278190080)
    self.assertEqual(dsm_net.MergeText(addition), addition)

    dsm_net = summarizer.DSMNet(167772160, 4278190080, existing_comment)
    self.assertEqual(dsm_net.MergeText(existing_comment), existing_comment)

    dsm_net = summarizer.DSMNet(167772160, 4278190080, existing_comment)
    self.assertEqual(dsm_net.MergeText(addition),
                     existing_comment + ', ' + addition)

  def testOrder(self):
    nets = [
        # not discontinously summarizable with the other two
        nacaddr.IPv4('209.85.147.129/32'),
        # discontinuosly summarizable, but should come before the first one
        nacaddr.IPv4('74.125.20.129/32'),
        nacaddr.IPv4('74.125.21.129/32'),
    ]
    result = summarizer.Summarize(nets)
    self.assertEqual(result, [summarizer.DSMNet(1249711233, 4294967039),
                              summarizer.DSMNet(3512046465, 4294967295)
                              ])


if __name__ == '__main__':
  absltest.main()


# tests/lib/windows_advfirewall_test.py

# Copyright 2016 Google 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.
"""Unittest for windows_advfirewall rendering module."""

import datetime
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import windows
from capirca.lib import windows_advfirewall


GOOD_HEADER_OUT = """
header {
  comment:: "this is an out test acl"
  target:: windows_advfirewall out inet
}
"""

GOOD_HEADER_IN = """
header {
  comment:: "this is an in test acl"
  target:: windows_advfirewall in inet
}
"""

GOOD_SIMPLE = """
term good-simple {
  protocol:: tcp
  action:: accept
}
"""

GOOD_SIMPLE_WARNING = """
term good-simple-warning {
  protocol:: tcp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_ANYPROTO = """
term good-term-anyproto {
  source-address:: FOO
  destination-address:: FOO
  action:: accept
}
"""

GOOD_TERM_MISCPROTO = """
term good-term-miscproto {
  protocol:: vrrp
  action:: accept
}
"""

# Edge case: protocol value for hopopt is 0
GOOD_TERM_HOPOPT = """
term good-term-hopopt {
  protocol:: hopopt
  action:: accept
}
"""

GOOD_TERM_ICMP_TYPES = """
term good-term-icmp-types {
  protocol:: icmp
  icmp-type:: echo-reply unreachable time-exceeded
  action:: deny
}
"""

GOOD_TERM_ICMPV6 = """
term good-term-icmpv6 {
  protocol:: icmpv6
  action:: accept
}
"""

BAD_TERM_ICMP = """
term test-icmp {
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

BAD_TERM_ACTION = """
term bad-term-action {
  protocol:: icmp
  action:: undefined
}
"""

GOOD_TERM_TCP = """
term good-term-tcp {
  comment:: "Test term 1"
  destination-address:: PROD_NETWRK
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_LOG = """
term good-term-log {
  protocol:: tcp
  logging:: true
  action:: accept
}
"""

EXPIRED_TERM = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

NEXT_TERM = """
term next {
  action:: next
}
"""

NEXT_LOG_TERM = """
term next-log {
  logging:: true
  action:: next
}
"""

EXCEPTION_POLICY = """
header {
  comment:: "Header comment"
  target:: windows_advfirewall out inet
}

term accept-corpdns {
  comment:: "accept-corpdns comment1"
  comment:: "accept-corpdns comment2"
  destination-address:: CORP_ANYCAST_DNS
  destination-port:: DNS
  protocol:: udp
  action:: accept
}

term deny-to-google {
  comment:: "deny-to-google comment"
  destination-address:: INTERNAL
  action:: deny
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request', 'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request', 'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class WindowsAdvFirewallTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def assertTrue(self, strings, result, term):
    for string in strings:
      fullstring = 'netsh advfirewall firewall add rule %s' % (string)
      super().assertIn(
          fullstring, result,
          'did not find "%s" for %s\nGot:\n%s' % (fullstring, term, result))

  def testTcp(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-tcp enable=yes interfacetype=any dir=out localip=any'
         ' remoteip=10.0.0.0/8 remoteport=25 protocol=tcp action=allow'],
        result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWRK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testIcmp(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_ICMP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-icmp enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=icmpv4 action=allow'],
        result,
        'did not find actual term for good-term-icmp')

  def testIcmpTypes(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_ICMP_TYPES, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-icmp-types enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=icmpv4:0,any action=block',
         'name=o_good-term-icmp-types enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=icmpv4:3,any action=block',
         'name=o_good-term-icmp-types enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=icmpv4:11,any action=block'],
        result,
        'did not find actual term for good-term-icmp-types')

  def testBadIcmp(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + BAD_TERM_ICMP, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      str, acl)

  @mock.patch.object(windows.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired '
        'and will not be rendered.',
        'expired_test', 'out')

  @mock.patch.object(windows.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'),
        self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring',
        'out')

  def testMultiprotocol(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + MULTIPLE_PROTOCOLS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_multi-proto enable=yes interfacetype=any dir=out localip=any'
         ' remoteip=any protocol=tcp action=allow',
         'name=o_multi-proto enable=yes interfacetype=any dir=out localip=any'
         ' remoteip=any protocol=udp action=allow',
         'name=o_multi-proto enable=yes interfacetype=any dir=out localip=any'
         ' remoteip=any protocol=icmpv4 action=allow'],
        result,
        'did not find actual term for multi-proto')

  def testAnyProtocol(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_ANYPROTO, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-anyproto enable=yes interfacetype=any dir=out'
         ' localip=10.0.0.0/8 remoteip=10.0.0.0/8 protocol=any action=allow'],
        result,
        '"any" proto')

  def testMiscProtocol(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_MISCPROTO + GOOD_TERM_HOPOPT, self.naming),
                                                 EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-miscproto enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=112 action=allow',
         'name=o_good-term-hopopt enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=0 action=allow'],
        result,
        'explicit miscellaneous proto')

  def testBuildTokens(self):
    pol1 = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_IN + GOOD_SIMPLE, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_IN + GOOD_SIMPLE_WARNING, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


if __name__ == '__main__':
  absltest.main()


# tests/lib/windows_ipsec_test.py

# Copyright 2016 Google 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.
"""Unittest for windows_ipsec rendering module."""

import datetime
from absl.testing import absltest
from unittest import mock

from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import windows_ipsec


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: windows_ipsec test-filter
}
"""

GOOD_SIMPLE = """
term good-simple {
  protocol:: tcp
  action:: accept
}
"""

GOOD_SIMPLE_WARNING = """
term good-simple-warning {
  protocol:: tcp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  action:: accept
}
"""

BAD_TERM_ICMP = """
term test-icmp {
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

GOOD_TERM_TCP = """
term good-term-tcp {
  comment:: "Test term 1"
  destination-address:: PROD_NET
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

EXPIRED_TERM = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'stateless_reply',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
}

SUPPORTED_SUB_TOKENS = {'action': {'accept', 'deny'}}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class WindowsIPSecTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  # pylint: disable=invalid-name
  def assertTrue(self, strings, result, term):
    for string in strings:
      fullstring = 'netsh ipsec static add %s' % (string)
      super().assertIn(
          fullstring, result,
          'did not find "%s" for %s' % (fullstring, term))

  def testPolicy(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['policy name=test-filter-policy assign=yes'],
        result,
        'header')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NET')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testTcp(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['filteraction name=t_good-term-tcp-action action=permit',
         'filter filterlist=t_good-term-tcp-list mirrored=yes srcaddr=any '
         ' dstaddr=10.0.0.0 dstmask=8 dstport=25',
         'rule name=t_good-term-tcp-rule policy=test-filter'
         ' filterlist=t_good-term-tcp-list'
         ' filteraction=t_good-term-tcp-action'],
        result,
        'good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NET')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testIcmp(self):
    acl = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['filterlist name=t_good-term-icmp-list',
         'filteraction name=t_good-term-icmp-action action=permit',
         'filter filterlist=t_good-term-icmp-list mirrored=yes srcaddr=any '
         ' dstaddr=any',
         'rule name=t_good-term-icmp-rule policy=test-filter'
         ' filterlist=t_good-term-icmp-list'
         ' filteraction=t_good-term-icmp-action'],
        result,
        'good-term-icmp')

  @mock.patch.object(windows_ipsec.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and '
        'will not be rendered.', 'expired_test',
        'test-filter')

  @mock.patch.object(windows_ipsec.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'),
        self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring',
        'test-filter')

  def testMultiprotocol(self):
    acl = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + MULTIPLE_PROTOCOLS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['filterlist name=t_multi-proto-list',
         'filteraction name=t_multi-proto-action action=permit',
         'filter filterlist=t_multi-proto-list mirrored=yes srcaddr=any '
         ' dstaddr=any  protocol=tcp',
         'filter filterlist=t_multi-proto-list mirrored=yes srcaddr=any '
         ' dstaddr=any  protocol=udp',
         'filter filterlist=t_multi-proto-list mirrored=yes srcaddr=any '
         ' dstaddr=any  protocol=icmp',
         'rule name=t_multi-proto-rule policy=test-filter'
         ' filterlist=t_multi-proto-list filteraction=t_multi-proto-action'],
        result,
        'multi-proto')

  def testBuildTokens(self):
    pol1 = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_SIMPLE, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_SIMPLE_WARNING, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


if __name__ == '__main__':
  absltest.main()


# tests/lib/windows_test.py

# Copyright 2008 Google 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.

"""Unittest for windows acl rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import windows


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: windows test-filter
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

GOOD_WARNING_TERM = """
term good-warning-term {
  protocol:: tcp udp icmp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM = """
term good-term {
  source-port:: FOO
  destination-port:: BAR
  protocol:: tcp
  action:: accept
}
"""

TCP_ESTABLISHED_TERM = """
term tcp-established {
  source-port:: FOO
  destination-port:: BAR
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

UDP_ESTABLISHED_TERM = """
term udp-established-term {
  source-port:: FOO
  destination-port:: BAR
  protocol:: udp
  option:: established
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request', 'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request', 'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class WindowsGeneratorTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testBuildTokens(self):
    pol1 = windows.WindowsGenerator(
        policy.ParsePolicy(GOOD_HEADER + MULTIPLE_PROTOCOLS_TERM, self.naming),
        EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = windows.WindowsGenerator(policy.ParsePolicy(
        GOOD_HEADER + GOOD_WARNING_TERM, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testSkipEstablished(self):
    # self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['123']
    pol = windows.WindowsGenerator(policy.ParsePolicy(
        GOOD_HEADER + TCP_ESTABLISHED_TERM + GOOD_TERM, self.naming), EXP_INFO)
    self.assertEqual(len(pol.windows_policies[0][4]), 1)
    pol = windows.WindowsGenerator(policy.ParsePolicy(
        GOOD_HEADER + UDP_ESTABLISHED_TERM + GOOD_TERM, self.naming), EXP_INFO)
    self.assertEqual(len(pol.windows_policies[0][4]), 1)


if __name__ == '__main__':
  absltest.main()


# tests/unit/wrapwords_test.py

from capirca.lib.aclgenerator import WrapWords
import pytest

SINGLE_LINE_OVERFLOW_TEXT_LONG = \
    "http://github.com/google/capirca/commit/c5" + \
    "6ddf19e2679892ff078cf27aeb18310c2697ed This " + \
    "is a long header. It's long on purpose. It's " + \
    "purpose is to test that the splitting works co" + \
    "rrectly. It should be well over the line limit" + \
    ". If it is shorter, it would not test the limit."

SINGLE_LINE_OVERFLOW_TEXT_LONG_EXPECTED = [
    "http://github.com/google/capirca/commit/c56ddf19e2679892ff078cf27aeb18",
    "310c2697ed",
    "This is a long header. It's long on purpose. It's purpose is to test",
    "that the splitting works correctly. It should be well over the line",
    "limit. If it is shorter, it would not test the limit."
]

MULTI_LINE_OVERFLOW_TEXT_LONG = \
    "this is a veryveryveryveryveryveryveryveryver" + \
    "yveryveryveryveryveryveryveryveryveryveryvery" + \
    "veryveryveryveryveryveryveryveryveryveryveryv" + \
    "eryveryveryveryveryveryveryveryveryveryvery long word"

MULTI_LINE_OVERFLOW_TEXT_LONG_EXPECTED = [
    "this is a",
    "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryve",
    "ryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryvery",
    "veryveryveryveryveryveryvery",
    "long word"
]

NO_OVERFLOW_LONG = \
    "This " + \
    "is a long header. It's long on purpose. It's " + \
    "purpose is to test that the splitting works co" + \
    "rrectly. It should be well over the line limit" + \
    ". If it is shorter, it would not test the limit."

NO_OVERFLOW_LONG_EXPECTED = [
    "This is a long header. It's long on purpose. It's purpose is to test",
    "that the splitting works correctly. It should be well over the line",
    "limit. If it is shorter, it would not test the limit."
]

NO_OVERFLOW_SHORT = \
    "This is a short line of text"

NO_OVERFLOW_SHORT_EXPECTED = [
    "This is a short line of text"
]

@pytest.mark.parametrize("test_input,expected", [
    (NO_OVERFLOW_SHORT, NO_OVERFLOW_SHORT_EXPECTED),
    (NO_OVERFLOW_LONG, NO_OVERFLOW_LONG_EXPECTED),
    (SINGLE_LINE_OVERFLOW_TEXT_LONG, SINGLE_LINE_OVERFLOW_TEXT_LONG_EXPECTED),
    (MULTI_LINE_OVERFLOW_TEXT_LONG, MULTI_LINE_OVERFLOW_TEXT_LONG_EXPECTED)
]
                         )


def testWrapWords(test_input, expected):
  result = WrapWords([test_input], 70)
  assert all((res == exp for res, exp in zip(result, expected)))


# tests/utils/iputils_test.py

import pytest

import pathlib

from capirca.utils import iputils
from capirca.lib import nacaddr


file_directory = pathlib.Path(__file__).parent.absolute()
exclude_address_testcases = []
with open(str(file_directory)+"/address_exclude_test_cases.txt", 'r') as f:
  for line in f:
    ipstr, exstrs, restrs = line.strip().split(' ')
    ip = nacaddr.IP(ipstr)
    exclude_ips = list(map(nacaddr.IP, exstrs.split(',')))
    expected_results = []
    for i in restrs.split(';'):
      result_strings = i.split(',')
      ip_map = map(nacaddr.IP, result_strings)
      ip_list = list(ip_map)
      expected_results.append(ip_list)
    for ex, res in zip(exclude_ips, expected_results):
      exclude_address_testcases.append((ip, ex, res))

class TestIPUtils:
  @pytest.mark.unit
  @pytest.mark.parametrize("ip,exclude,expected", exclude_address_testcases)
  def test_exclude_address(self, ip, exclude, expected):
    result = iputils.exclude_address(ip, exclude)

    assert list(result) == expected


# tools/cgrep.py

# Copyright 2011 Google 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.
#

"""Simple util to grep through network and service definitions.

Examples:
  To find out which tokens contain "10.4.3.1" use
  $ cgrep.py -i 10.4.3.1

  To find out if token 'FOO' includes ip "1.2.3.4" use
  $ cgrep.py -t FOO -i 1.2.3.4

  To find the difference and union of tokens 'FOO' and 'BAR' use
  $ cgrep.py -c FOO BAR

  To find the difference of network tokens to which 2 IPs belong use
  $ cgrep.py -g 1.1.1.1 2.2.2.2

  To find which IPs are in the 'FOO' network token use
  $ cgrep.py -o FOO

  To find which port & protocol pairs are in a service token 'FOO' use
  $ cgrep.py -s FOO

  To find which service tokens contain port '22' and protocol 'tcp' use
  $ cgrep.py -p 22 tcp
"""

import argparse
import pprint
import sys

from absl import app
from absl import logging
from capirca.lib import nacaddr
from capirca.lib import naming


def is_valid_ip(arg):
  """Validates a value to be an IP or not.

  Args:
    arg: potential IP address as a string.

  Returns:
    arg as IP object (if arg is an IP)

  Raises:
    Error (if arg is not an IP)
  """
  try:
    nacaddr.IP(arg)
  except:
    raise argparse.ArgumentTypeError('%s is an invalid ip address' % arg)
  return arg


def cli_options():
  """Builds the argparse options for cgrep.

  TODO(robankeny): Move this to flags.

  Returns:
    parser: the arguments, ready to be parsed.
  """

  parser = argparse.ArgumentParser(
      description='c[apirca]grep',
      formatter_class=argparse.RawTextHelpFormatter
  )

  parser.add_argument('-d', '--def', dest='defs',
                      help='Network Definitions directory location. \n',
                      default='./def')

  # -i and -t can be used together, but not with any other option.
  ip_group = parser.add_argument_group()
  # take 1 or more IPs
  ip_group.add_argument('-i', '--ip', dest='ip', nargs='+', type=is_valid_ip,
                        help='Return list of definitions containing the '
                        'IP(s).\nMultiple IPs permitted.')

  ip_group.add_argument('-t', '--token', dest='token',
                        help=('See if an IP is contained within the given '
                              'token.\nMust be used in conjunction with '
                              '-i/--ip [addr].'))

  exclusive_group = parser.add_mutually_exclusive_group()
  # the rest of the arguments are mutually exclusive with each other,
  # and -i / -t
  exclusive_group.add_argument('-c', '--cmp', dest='cmp', nargs=2,
                               metavar=('OBJ', 'OBJ'),
                               help=('Compare the two given network '
                                     'definition tokens'))

  exclusive_group.add_argument('-g', '--gmp', dest='gmp', nargs=2,
                               type=is_valid_ip, metavar=('IP', 'IP'),
                               help=('Diff the network objects to'
                                     ' which the given IP(s) belong'))

  exclusive_group.add_argument('-o', '--obj', dest='obj', nargs='+',
                               help=('Return list of IP(s) contained within '
                                     'the given token(s)'))

  exclusive_group.add_argument('-s', '--svc', dest='svc', nargs='+',
                               help=('Return list of port(s) contained '
                                     'within given token(s)'))

  exclusive_group.add_argument('-p', '--port', dest='port', nargs=2,
                               metavar=('PORT', 'PROTO'),
                               help=('Returns a list of tokens containing '
                                     'the given port and protocol'))

  return parser


def main(argv):
  """Determines the code path based on the arguments passed."""
  del argv  # Unused.
  parser = cli_options()
  options = parser.parse_args()
  db = naming.Naming(options.defs)
  p = pprint.PrettyPrinter(indent=1, depth=4, width=1).pprint

  # if -i and any other option:
  if options.ip and any([options.gmp, options.cmp, options.obj, options.svc,
                         options.port]):
    logging.info('You can only use -i with -t or by itself')

  # if -i and -t
  elif options.token and options.ip:
    try:
      get_nets([options.token], db)
    except naming.UndefinedAddressError:
      logging.info("Network group '%s' is not defined!", options.token)
    else:
      results = compare_ip_token(options, db)
      logging.info(results)

  # if -t, but not -i; invalid!
  elif options.token and not options.ip:
    logging.info('You must specify an IP Address with -i [addr]')

  # if -i
  elif options.ip:
    for ip in options.ip:
      groups = get_ip_parents(ip, db)
      logging.info('Results for IP: %s', ip)
      # iterate and print the tokens we found.
      for name, networks in groups:
        # print the group name [0], and the networks it was in [1]
        logging.info('%s  %s', name, networks)

  elif options.gmp:
    common, diff1, diff2 = group_diff(options, db)
    print_diff(options.gmp[0], common, diff1, diff2)
    logging.info('')
    print_diff(options.gmp[1], common, diff2, diff1)

  # if -c
  elif options.cmp:
    meta, results = compare_tokens(options, db)
    first_name = meta[0]
    second_name = meta[1]
    union = meta[2]
    logging.info('Union of %s and %s:\n %s\n', first_name, second_name, union)
    logging.info('Diff of %s and %s:', first_name, second_name)
    for i in results:
      logging.info(' %s', i)
    logging.info('')
    first_obj, sec_obj = options.cmp
    if check_encapsulated('network', first_obj, sec_obj, db):
      logging.info('%s fully encapsulates %s', sec_obj, first_obj)
    else:
      logging.info('%s does _not_ fully encapsulate %s', sec_obj, first_obj)
    # check the other way around.
    if check_encapsulated('network', sec_obj, first_obj, db):
      logging.info('%s fully encapsulates %s', first_obj, sec_obj)
    else:
      logging.info('%s does _not_ fully encapsulate %s', first_obj, sec_obj)

  # if -o
  elif options.obj:
    for obj in options.obj:
      try:
        token, ips = get_nets([obj], db)[0]
      except naming.UndefinedAddressError:
        logging.info('%s is an invalid object', obj)
      else:
        logging.info('%s:', token)
        # convert list of ip objects to strings and sort them
        ips.sort(key=lambda x: int(x.ip))
        p([str(x) for x in ips])

  # if -s
  elif options.svc:
    try:
      results = get_ports(options.svc, db)
    except naming.UndefinedServiceError:
      logging.info('%s contains an invalid service object', str(options.svc))
    else:
      for result in get_ports(options.svc, db):
        svc, port = result
        logging.info('%s:', svc)
        p(port)

  # if -p
  elif options.port:
    port, protocol, result = get_services(options, db)
    logging.info('%s/%s:', port, protocol)
    p(result)

  # if nothing is passed
  elif not any((options.cmp, options.ip, options.token, options.obj,
                options.svc, options.port)):
    parser.print_help()
  logging.info('')


def check_encapsulated(obj_type, first_obj, second_obj, db):
  """Checks if a network/service object is entirely contained within another.

  Args:
    obj_type: "network" or "service"
    first_obj: The name of the first network/service object
    second_obj: The name of the secondnetwork/service object
    db: The network and service definitions

  Returns:
    Error or bool:
      ValueError if an invalid object type is passed
      True if the first_obj is entirely within second_obj, otherwise False

  Raises:
    ValueError: When value is not a network or service.
  """
  if obj_type == 'network':
    # the indexing is to get the list of networks out of the tuple[1] and
    # list[0] returned by get_nets
    first = get_nets([first_obj], db)[0][1]
    second = get_nets([second_obj], db)[0][1]

  elif obj_type == 'service':
    first = get_ports([first_obj], db)[0][1]
    second = get_ports([second_obj], db)[0][1]
  else:
    raise ValueError("check_encapsulated() currently only supports "
                     "'network' and 'service' for the obj_type parameter")
  # iterates over each object in the first group, and then each obj in the
  # second group, making sure each one in the first is contained
  # somewhere in the second.
  for obj in first:
    for sec_obj in second:
      if obj.version == sec_obj.version:
        if obj.subnet_of(sec_obj):
          break
    # if we got through every object in the second group, and didn't have
    # a match, then the first group is not entirely contained.
    else:
      return False
  # if we got here, then the group was fully contained.
  return True


def print_diff(ip, common, diff1, diff2):
  """Print out the common, added, and removed network objects between 2 IPs.

  Args:
    ip: the IP being compared against
    common: the network objects shared between the two IPs
                    ('ip' and the other passed into options.cmp)
    diff1: the network objects present in 'ip' but not in the other IP
                   passed into options.cmp
    diff2: the network objects not present in 'ip' but are present in
                   the other IP passed into options.cmp
  """
  logging.info('IP: %s', ip)
  if common:
    common = ['  {0}'.format(elem) for elem in common]
    logging.info('\n'.join(common))
  if diff1:
    diff = ['+ {0}'.format(elem) for elem in diff1]
    logging.info('\n'.join(diff))
  if diff2:
    diff = ['- {0}'.format(elem) for elem in diff2]
    logging.info('\n'.join(diff))


def group_diff(options, db):
  """Diffs two different group objects.

  Args:
    options: the options sent to the script
    db : network and service definitions

  Returns:
    tuple: the common lines, the differences from 1 to 2,
                          and the differences from 2 to 1
  """
  nested_rvals = []
  for ip in options.gmp:
    nested_rvals.append(get_ip_parents(ip, db))
  # get just the list of groups, stripping out the networks.
  group1 = [x[0] for x in nested_rvals[0]]
  group2 = [x[0] for x in nested_rvals[1]]
  common = sorted(list(set(group1) & set(group2)))
  diff1 = sorted(list(set(group1) - set(group2)))
  diff2 = sorted(list(set(group2) - set(group1)))
  return common, diff1, diff2


def get_ip_parents(ip, db):
  """Gets a list of all network objects that include an IP.

  Args:
    ip: the IP we're looking for the parents of
    db: network and service definitions

  Returns:
    results: a list of all groups that include the IP, in the format:
                     [("Group", ["networks", "matched"]), (etc)]
  """
  results = []
  rval = db.GetIpParents(ip)
  for v in rval:
    nested = db.GetNetParents(v)
    prefix_and_nets = get_nets_and_highest_prefix(ip, v, db)
    if nested:
      for n in nested:
        results.append(('%s -> %s' % (n, v), prefix_and_nets))
    else:
      results.append((v, prefix_and_nets))
  # sort the results by prefix length descending
  results = sorted(results, key=lambda x: x[1][0], reverse=True)
  # strip out the no longer needed prefix lengths before handing off
  for index, group in enumerate(results):
    results[index] = (group[0], group[1][1])
  return results


def get_nets_and_highest_prefix(ip, net_group, db):
  """Find the highest prefix length in all networks given it contains the IP.

  Args:
    ip: the IP address contained in net_group
    net_group: the name of the network object we'll be looking through
    db: network and service definitions

  Returns:
    highest_prefix_length, networks as tuple
      highest_prefix_length : the longest prefix length found,
      networks : network objects
  """
  highest_prefix_length = 0
  networks = []
  ip = nacaddr.IP(ip)
  # loop through all the networks in the net_group
  for net in get_nets([net_group], db)[0][1]:
    # find the highest prefix length for the networks that contain the IP
    if ip.version == net.version:
      if ip.subnet_of(net):
        networks.append(str(net))
        if net.prefixlen > highest_prefix_length:
          highest_prefix_length = net.prefixlen
  return highest_prefix_length, networks


def get_nets(objects, db):
  """Gets a list of all networks that are inside of a network object.

  Args:
    objects: network objects
    db: network and service definitions

  Returns:
    results : all networks inside a network object
  """
  results = []
  for obj in objects:
    net = db.GetNet(obj)
    results.append((obj, net))
  return results


def compare_tokens(options, db):
  """Compares to network objects against each other.

  Args:
    options: the options sent to the script
    db: network and service definitions

  Returns:
    meta, results :
      ((first object, second object, union of those two),
       diff of those two network objects)
  """
  t1, t2 = options.cmp
  d1 = db.GetNet(t1)
  d2 = db.GetNet(t2)
  union = list(set(d1 + d2))
  meta = (t1, t2, union)
  results = []
  for el in set(d1 + d2):
    el = nacaddr.IP(el)
    if el in d1 and el in d2:
      results.append(str(el))
    elif el in d1:
      results.append(str(el))
    elif el in d2:
      results.append(str(el))
  return meta, results


def compare_ip_token(options, db):
  """Looks to see if a network IP is contained in a network object.

  Args:
    options: the options sent to the script
    db: network and service definitions

  Returns:
    results : end-user string stating the results
  """
  token = options.token
  results = []
  for ip in options.ip:
    rval = db.GetIpParents(ip)
    if token in rval:
      results = '%s is in %s' % (ip, token)
    else:
      results = '%s is _not_ in %s' % (ip, token)
  return results


def get_ports(svc_group, db):
  """Gets the ports and protocols defined in a service group.

  Args:
    svc_group: a list of strings for each service group
    db: network and service definitions

  Returns:
    results: a list of tuples for each service defined, in the format:
                     (service name, "<port>/<protocol>")
  """
  results = []
  for svc in svc_group:
    port = db.GetService(svc)
    results.append((svc, port))
  return results


def get_services(options, db):
  """Finds any services with that include a specific port/protocol pair.

  Args:
    options: the options sent to the script
    db: network and service definitions

  Returns:
    port, protocol, results as tuple in the format:
    (port, protocol, list of the services containing this pair)
  """
  results = []
  port, protocol = options.port
  # swap values if they were passed in wrong order
  if port.isalpha() and protocol.isdigit():
    port, protocol = protocol, port
  results = db.GetPortParents(port, protocol)
  return port, protocol, results


if __name__ == '__main__':
  app.run(main, argv=sys.argv[:1])


# tools/iputilstools.py

import random
import ipaddress
import itertools as it


def write_excludes_testcase(ipstr, excludelist='', max_prefix_range=8, max_random_subnets=30):
    """
    Writes a testcase to the tests/utils/address_exclude_test_cases.txt file.
    Note that the number of prefixes to consider grows exponentially, so unless
    you *do* want to consider a large pool to randomly select from, keep it at the default

    Args:
      ipstr: the ip network as a string (v4 or v6) to base the test on.
      excludelist: optional comma-separated string of ip networks to exclude
      max_prefix_range: the largest number of prefixes to consider.
      max_random_subnets: the number of subnets to do exclusion tests for, if randomly generating
    Returns:
      None
    """
    ip = ipaddress.ip_network(ipstr)
    if len(excludelist) == 0:  # empty excludelist, making a random one
        prefixrange = min(max_prefix_range, ip.max_prefixlen - ip.prefixlen)
        excludelist = it.chain.from_iterable(ip.subnets(i) for i in range(1, prefixrange+1))
        total_ips = 2**prefixrange
        ip_positions = set(
            random.choices(
                range(total_ips),
                k=min(
                    max_random_subnets,
                    total_ips
                )
            )
        )
        compress_map = (1 if i in ip_positions else 0 for i in range(total_ips))
        excludelist = list(it.compress(excludelist, compress_map))
    else:
        excludelist = list(map(ipaddress.ip_network, excludelist.split(',')))

    result_list = []
    for address in excludelist:
        result_list.append(ip.address_exclude(address))

    ipst = str(ip)
    exst = ",".join(map(str, excludelist))
    rest = ";".join(",".join(map(str, sorted(result))) for result in result_list)
    with open('tests/utils/address_exclude_test_cases.txt', 'a') as f:
        f.write("%s %s %s\n" % (ipst, exst, rest))


# capirca/utils/config.py

"""A module to handle merging file configurations with CLI configs for Capirca."""

import yaml

defaults = {
    'base_directory': './policies',
    'definitions_directory': './def',
    'policy_file': None,
    'output_directory': './',
    'optimize': False,
    'recursive': True,
    'debug': False,
    'verbose': False,
    'ignore_directories': ['DEPRECATED', 'def'],
    'max_renderers': 10,
    'shade_check': False,
    'exp_info': 2
}


def yaml_loader(filename):
  with open(filename, 'r') as f:
    try:
      data = yaml.safe_load(f)
    except AttributeError:
      data = yaml.safe_load(f)

  return data


def flags_to_dict(absl_flags):
  base = {
      'base_directory': absl_flags.base_directory,
      'definitions_directory': absl_flags.definitions_directory,
      'policy_file': absl_flags.policy_file,
      'output_directory': absl_flags.output_directory,
      'optimize': absl_flags.optimize,
      'recursive': absl_flags.recursive,
      'debug': absl_flags.debug,
      'verbose': absl_flags.verbose,
      'ignore_directories': absl_flags.ignore_directories,
      'max_renderers': absl_flags.max_renderers,
      'shade_check': absl_flags.shade_check,
      'exp_info': absl_flags.exp_info,
  }

  return {
      flag: base[flag] for flag in filter(lambda f: base[f] is not None, base)
  }


def merge_files(*files):
  result = {}

  for item in files:
    data = yaml_loader(item)
    result.update(data)

  return {
      flag: result[flag]
      for flag in filter(lambda f: result[f] is not None, result)
  }


def generate_configs(absl_flags):
  cli_configs = flags_to_dict(absl_flags)
  if absl_flags.config_file:
    file_configs = merge_files(*absl_flags.config_file)
  else:
    file_configs = {}

  result = defaults.copy()
  result.update(cli_configs)
  result.update(file_configs)

  return result


# capirca/utils/iputils.py

"""A module of utilities to work with IP addresses in a faster way."""

import ipaddress


def exclude_address(
    base_net: ipaddress._BaseNetwork,    # pylint disable=protected-access
    exclude_net: ipaddress._BaseNetwork  # pylint disable=protected-access
):
  """
  Function to exclude a subnetwork from another, returning a generator that
  yields all values that correspond to the base network without the exclude
  network.

  This is functionally equivalent to the _BaseNetwork "address_exclude" from the
  `ipaddress` standard library, but is a faster implementation since
  the standard library function is a O(n) operation on the length of the
  netmask of the excluding network, whereas this function is O(1) for all cases.

  Args:
    base_net: an object of type _BaseNetwork, the network that
              contains the exclude network
    exclude_net: an object of type _BaseNetwork, the network
                 that is being removed from the base_net
  Raises:
    ValueError if exclude_net is not completely contained in base_net

  Yields:
    A sequence of IP networks that do not encompass the exclude_net
  """

  if not isinstance(base_net, ipaddress._BaseNetwork):  # pylint disable=protected-access
    raise TypeError('%s is not a network object' % base_net)

  if not isinstance(exclude_net, ipaddress._BaseNetwork):  # pylint disable=protected-access
    raise TypeError('%s is not a network object' % exclude_net)

  if not base_net._version == exclude_net._version:  # pylint disable=protected-access # pytype: disable=attribute-error
    raise TypeError(
      '%s and %s are not of the same version' % (base_net, exclude_net)
    )

  if not exclude_net.subnet_of(base_net): # pytype: disable=attribute-error
    raise ValueError()
  if exclude_net == base_net:
    return

  include_range = base_net.network_address._ip, base_net.broadcast_address._ip  # pylint disable=protected-access # pytype: disable=attribute-error
  exclude_range = exclude_net.network_address._ip, exclude_net.broadcast_address._ip  # pylint disable=protected-access # pytype: disable=attribute-error
  address_class = base_net.network_address.__class__  # pylint disable=protected-access
  if include_range[0] == exclude_range[0]:
    result_start = address_class(exclude_range[1] + 1)
    result_end = address_class(include_range[1])
    for address in ipaddress.summarize_address_range(result_start, result_end):
      yield address
  elif include_range[1] == exclude_range[1]:
    result_start = address_class(include_range[0])
    result_end = address_class(exclude_range[0] - 1)
    for address in ipaddress.summarize_address_range(result_start, result_end):
      yield address
  else:
    first_section_start = address_class(include_range[0])
    first_section_end = address_class(exclude_range[0] - 1)
    second_section_start = address_class(exclude_range[1] + 1)
    second_section_end = address_class(include_range[1])
    for address in ipaddress.summarize_address_range(first_section_start, first_section_end):
      yield address
    for address in ipaddress.summarize_address_range(second_section_start, second_section_end):
      yield address


# aclcheck_cmdline.py

# Copyright 2011 Google 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.
#

"""Command line interface to aclcheck library."""

from optparse import OptionParser

from capirca.lib import aclcheck
from capirca.lib import naming
from capirca.lib import policy


def main():
  # TODO(robankeny): Lets move this to gflags
  usage = 'usage: %prog [options] arg'
  _parser = OptionParser(usage)
  _parser.add_option('--definitions-directory', dest='definitions',
                     help='definitions directory', default='./def')
  _parser.add_option('-p', '--policy-file', dest='pol',
                     help='policy file', default='./policies/sample.pol')
  _parser.add_option('-d', '--destination', dest='dst',
                     help='destination IP', default='200.1.1.1')
  _parser.add_option('-s', '--source', dest='src',
                     help='source IP', default='any')
  _parser.add_option('--proto', '--protocol', dest='proto',
                     help='Protocol (tcp, udp, icmp, etc.)', default='tcp')
  _parser.add_option('--dport', '--destination-port', dest='dport',
                     help='destination port', default='80')
  _parser.add_option('--sport', '--source-port', dest='sport',
                     help='source port', default='1025')
  (FLAGS, unused_args) = _parser.parse_args()

  defs = naming.Naming(FLAGS.definitions)
  policy_obj = policy.ParsePolicy(open(FLAGS.pol).read(), defs)
  check = aclcheck.AclCheck(policy_obj, src=FLAGS.src, dst=FLAGS.dst,
                            sport=FLAGS.sport, dport=FLAGS.dport,
                            proto=FLAGS.proto)
  print(str(check))

if __name__ == '__main__':
  main()


# setup.py

#
# Copyright 2009 Google Inc.
#
# 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.
"""capirca installation module."""

from os import path
import setuptools

root_dir = path.abspath(path.dirname(__file__))

with open(path.join(root_dir, 'VERSION'), encoding='utf-8') as f:
  version = f.readline().strip()

with open(path.join(root_dir, 'README.md'), encoding='utf-8') as f:
  long_description = f.read()

setuptools.setup(
    name='capirca',
    version=version,
    description='Capirca',
    long_description=long_description,
    long_description_content_type='text/markdown',
    license='Apache License, Version 2.0',
    url='https://github.com/google/capirca/',
    maintainer='Capirca Team',
    maintainer_email='capirca-dev@google.com',
    packages=setuptools.find_packages(exclude=['tests*']),
    zip_safe=False,
    entry_points={
        'console_scripts': ['aclgen = capirca.aclgen:EntryPoint'],
    },
    classifiers=[
        'Development Status :: 5 - Production/Stable',
        'License :: OSI Approved :: Apache Software License',
        'Operating System :: OS Independent',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.6',
        'Topic :: Security',
        'Topic :: System :: Networking :: Firewalls',
    ],
    install_requires=[
        'absl-py',
        'ply',
        'ipaddress>=1.0.22',
        'mock',
        'six',
        'PyYAML',
    ],
    python_requires='>=3.6',
)


# capirca/__init__.py

"""Capirca."""


# capirca/aclgen.py

# Copyright 2011 Google 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.
#
"""Renders policy source files into actual Access Control Lists."""

import copy
import multiprocessing
import pathlib
import sys
from typing import Iterator, List, Tuple

from absl import app
from absl import flags
from absl import logging

from capirca.lib import aclgenerator
from capirca.lib import arista
from capirca.lib import arista_tp
from capirca.lib import aruba
from capirca.lib import brocade
from capirca.lib import cisco
from capirca.lib import ciscoasa
from capirca.lib import cisconx
from capirca.lib import ciscoxr
from capirca.lib import cloudarmor
from capirca.lib import gce
from capirca.lib import gcp_hf
from capirca.lib import ipset
from capirca.lib import iptables
from capirca.lib import juniper
from capirca.lib import junipermsmpc
from capirca.lib import junipersrx
from capirca.lib import k8s
from capirca.lib import naming
from capirca.lib import nftables
from capirca.lib import nsxv
from capirca.lib import openconfig
from capirca.lib import packetfilter
from capirca.lib import paloaltofw
from capirca.lib import pcap
from capirca.lib import policy
from capirca.lib import speedway
from capirca.lib import srxlo
from capirca.lib import windows_advfirewall

from capirca.utils import config

FLAGS = flags.FLAGS
WriteList = List[Tuple[pathlib.Path, str]]


def SetupFlags():
  """Read in configuration from CLI flags."""
  flags.DEFINE_string(
      'base_directory', None, 'The base directory to look for acls; '
      'typically where you\'d find ./corp and ./prod\n(default: \'%s\')' %
      config.defaults['base_directory'])
  flags.DEFINE_string(
      'definitions_directory', None,
      'Directory where the definitions can be found.\n(default: \'%s\')' %
      config.defaults['definitions_directory'])
  flags.DEFINE_string('policy_file', None,
                      'Individual policy file to generate.')
  flags.DEFINE_string(
      'output_directory', None,
      'Directory to output the rendered acls.\n(default: \'%s\')' %
      config.defaults['output_directory'])
  flags.DEFINE_boolean(
      'optimize',
      None,
      'Turn on optimization.\n(default: \'%s\')' % config.defaults['optimize'],
      short_name='o')
  flags.DEFINE_boolean(
      'recursive', None,
      'Descend recursively from the base directory rendering acls\n(default: \'%s\')'
      % str(config.defaults['recursive']).lower())
  flags.DEFINE_boolean(
      'debug', None, 'Debug messages\n(default: \'%s\')' %
      str(config.defaults['debug']).lower())
  flags.DEFINE_boolean(
      'verbose', None, 'Verbose messages\n(default: \'%s\')' %
      str(config.defaults['verbose']).lower())
  flags.DEFINE_list(
      'ignore_directories', None,
      'Don\'t descend into directories that look like this string\n(default: \'%s\')'
      % ','.join(config.defaults['ignore_directories']))
  flags.DEFINE_integer(
      'max_renderers', None,
      'Max number of rendering processes to use.\n(default: \'%s\')' %
      config.defaults['max_renderers'])
  flags.DEFINE_boolean(
      'shade_check', None,
      'Raise an error when a term is completely shaded by a prior term.\n(default: \'%s\')'
      % str(config.defaults['shade_check']).lower())
  flags.DEFINE_integer(
      'exp_info', None,
      'Print a info message when a term is set to expire in that many weeks.\n(default: \'%s\')'
      % str(config.defaults['exp_info']))
  flags.DEFINE_multi_string(
      'config_file', None,
      'A yaml file with the configuration options for capirca')


class Error(Exception):
  """Base Error class."""


class P4WriteFileError(Error):
  """Error when there are issues p4 editing the destination."""


class ACLGeneratorError(Error):
  """Raised when an ACL generator has errors."""


class ACLParserError(Error):
  """Raised when the ACL parser fails."""


def SkipLines(text, skip_line_func=False):
  """Apply skip_line_func to the given text.

  Args:
    text: list of the first text to scan
    skip_line_func: function to use to check if we should skip a line

  Returns:
    ret_text: text(list) minus the skipped lines
  """
  if not skip_line_func:
    return text
  return [x for x in text if not skip_line_func(x)]


def RenderFile(base_directory: str, input_file: pathlib.Path,
               output_directory: pathlib.Path, definitions: naming.Naming,
               exp_info: int, optimize: bool, shade_check: bool,
               write_files: WriteList):
  """Render a single file.

  Args:
    base_directory: The base directory to look for acls.
    input_file: the name of the input policy file.
    output_directory: the directory in which we place the rendered file.
    definitions: the definitions from naming.Naming().
    exp_info: print a info message when a term is set to expire in that many
      weeks.
    optimize: a boolean indicating if we should turn on optimization or not.
    shade_check: should we raise an error if a term is completely shaded
    write_files: a list of file tuples, (output_file, acl_text), to write
  """
  output_relative = input_file.relative_to(base_directory).parent.parent
  output_directory = output_directory / output_relative

  logging.debug('rendering file: %s into %s', input_file, output_directory)

  pol = None
  jcl = False
  acl = False
  atp = False
  asacl = False
  aacl = False
  bacl = False
  eacl = False
  gca = False
  gcefw = False
  gcphf = False
  ips = False
  ipt = False
  msmpc = False
  spd = False
  nsx = False
  oc = False
  pcap_accept = False
  pcap_deny = False
  pf = False
  srx = False
  jsl = False
  nft = False
  win_afw = False
  nxacl = False
  xacl = False
  paloalto = False
  k8s_pol = False

  try:
    with open(input_file) as f:
      conf = f.read()
      logging.debug('opened and read %s', input_file)
  except IOError as e:
    logging.warning('bad file: \n%s', e)
    raise

  try:
    pol = policy.ParsePolicy(
        conf,
        definitions,
        optimize=optimize,
        base_dir=base_directory,
        shade_check=shade_check)
  except policy.ShadingError as e:
    logging.warning('shading errors for %s:\n%s', input_file, e)
    return
  except (policy.Error, naming.Error):
    raise ACLParserError('Error parsing policy file %s:\n%s%s' %
                         (input_file, sys.exc_info()[0], sys.exc_info()[1]))

  platforms = set()
  for header in pol.headers:
    platforms.update(header.platforms)

  if 'juniper' in platforms:
    jcl = copy.deepcopy(pol)
  if 'cisco' in platforms:
    acl = copy.deepcopy(pol)
  if 'ciscoasa' in platforms:
    asacl = copy.deepcopy(pol)
  if 'brocade' in platforms:
    bacl = copy.deepcopy(pol)
  if 'arista' in platforms:
    eacl = copy.deepcopy(pol)
  if 'arista_tp' in platforms:
    atp = copy.deepcopy(pol)
  if 'aruba' in platforms:
    aacl = copy.deepcopy(pol)
  if 'ipset' in platforms:
    ips = copy.deepcopy(pol)
  if 'iptables' in platforms:
    ipt = copy.deepcopy(pol)
  if 'msmpc' in platforms:
    msmpc = copy.deepcopy(pol)
  if 'nsxv' in platforms:
    nsx = copy.deepcopy(pol)
  if 'openconfig' in platforms:
    oc = copy.deepcopy(pol)
  if 'packetfilter' in platforms:
    pf = copy.deepcopy(pol)
  if 'pcap' in platforms:
    pcap_accept = copy.deepcopy(pol)
    pcap_deny = copy.deepcopy(pol)
  if 'speedway' in platforms:
    spd = copy.deepcopy(pol)
  if 'srx' in platforms:
    srx = copy.deepcopy(pol)
  if 'srxlo' in platforms:
    jsl = copy.deepcopy(pol)
  if 'windows_advfirewall' in platforms:
    win_afw = copy.deepcopy(pol)
  if 'cisconx' in platforms:
    nxacl = copy.deepcopy(pol)
  if 'ciscoxr' in platforms:
    xacl = copy.deepcopy(pol)
  if 'nftables' in platforms:
    nft = copy.deepcopy(pol)
  if 'gce' in platforms:
    gcefw = copy.deepcopy(pol)
  if 'gcp_hf' in platforms:
    gcphf = copy.deepcopy(pol)
  if 'paloalto' in platforms:
    paloalto = copy.deepcopy(pol)
  if 'cloudarmor' in platforms:
    gca = copy.deepcopy(pol)
  if 'k8s' in platforms:
    k8s_pol = copy.deepcopy(pol)

  acl_obj: aclgenerator.ACLGenerator

  try:
    if jcl:
      acl_obj = juniper.Juniper(jcl, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if msmpc:
      acl_obj = junipermsmpc.JuniperMSMPC(msmpc, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if srx:
      acl_obj = junipersrx.JuniperSRX(srx, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if acl:
      acl_obj = cisco.Cisco(acl, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if asacl:
      acl_obj = ciscoasa.CiscoASA(asacl, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if aacl:
      acl_obj = aruba.Aruba(aacl, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if bacl:
      acl_obj = brocade.Brocade(bacl, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if eacl:
      acl_obj = arista.Arista(eacl, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if atp:
      acl_obj = arista_tp.AristaTrafficPolicy(atp, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if ips:
      acl_obj = ipset.Ipset(ips, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if ipt:
      acl_obj = iptables.Iptables(ipt, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if nsx:
      acl_obj = nsxv.Nsxv(nsx, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if oc:
      acl_obj = openconfig.OpenConfig(oc, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if spd:
      acl_obj = speedway.Speedway(spd, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if pcap_accept:
      acl_obj = pcap.PcapFilter(pcap_accept, exp_info)
      RenderACL(
          str(acl_obj), '-accept' + acl_obj.SUFFIX, output_directory,
          input_file, write_files)
    if pcap_deny:
      acl_obj = pcap.PcapFilter(pcap_deny, exp_info, invert=True)
      RenderACL(
          str(acl_obj), '-deny' + acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if pf:
      acl_obj = packetfilter.PacketFilter(pf, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if win_afw:
      acl_obj = windows_advfirewall.WindowsAdvFirewall(win_afw, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if jsl:
      acl_obj = srxlo.SRXlo(jsl, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if nxacl:
      acl_obj = cisconx.CiscoNX(nxacl, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if xacl:
      acl_obj = ciscoxr.CiscoXR(xacl, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if nft:
      acl_obj = nftables.Nftables(nft, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if gcefw:
      acl_obj = gce.GCE(gcefw, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if gcphf:
      acl_obj = gcp_hf.HierarchicalFirewall(gcphf, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)

    if paloalto:
      acl_obj = paloaltofw.PaloAltoFW(paloalto, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)
    if gca:
      acl_obj = cloudarmor.CloudArmor(gca, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)

    if k8s_pol:
      acl_obj = k8s.K8s(k8s_pol, exp_info)
      RenderACL(
          str(acl_obj), acl_obj.SUFFIX, output_directory, input_file,
          write_files)

  # TODO(robankeny) add additional errors.
  except (
      juniper.Error,
      junipermsmpc.Error,
      junipersrx.Error,
      cisco.Error,
      ipset.Error,
      iptables.Error,
      speedway.Error,
      pcap.Error,
      aclgenerator.Error,
      aruba.Error,
      nftables.Error,
      gce.Error,
      cloudarmor.Error,
      k8s.Error) as e:
    raise ACLGeneratorError('Error generating target ACL for %s:\n%s' %
                            (input_file, e))


def RenderACL(acl_text: str,
              acl_suffix: str,
              output_directory: pathlib.Path,
              input_file: pathlib.Path,
              write_files: List[Tuple[pathlib.Path, str]],
              binary: bool = False):
  """Write the ACL string out to file if appropriate.

  Args:
    acl_text: Rendered output of an ACL Generator.
    acl_suffix: File suffix to append to output filename.
    output_directory: The directory to write the output file.
    input_file: The name of the policy file that was used to render ACL.
    write_files: A list of file tuples, (output_file, acl_text), to write.
    binary: Boolean if the rendered ACL is in binary format.
  """
  input_filename = input_file.with_suffix(acl_suffix).name
  output_file = output_directory / input_filename

  if FilesUpdated(output_file, acl_text, binary):
    logging.info('file changed: %s', output_file)
    write_files.append((output_file, acl_text))
  else:
    logging.debug('file not changed: %s', output_file)


def FilesUpdated(file_name: pathlib.Path, new_text: str, binary: bool) -> bool:
  """Diff the rendered acl with what's already on disk.

  Args:
    file_name: Name of file on disk to check against.
    new_text: Text of newly generated ACL.
    binary: True if file is a binary format.

  Returns:
    Boolean if config does not equal new text.
  """
  if binary:
    readmode = 'rb'
  else:
    readmode = 'r'
  try:
    with open(file_name, readmode) as f:
      conf: str = str(f.read())
  except IOError:
    return True
  if not binary:
    p4_id = '$I d:'.replace(' ', '')
    p4_date = '$Da te:'.replace(' ', '')
    p4_revision = '$Rev ision:'.replace(' ', '')

    def P4Tags(text: str) -> bool:
      return not (p4_id in text or p4_date in text or p4_revision in text)

    filtered_conf = filter(P4Tags, conf.split('\n'))
    filtered_text = filter(P4Tags, new_text.split('\n'))
    return list(filtered_conf) != list(filtered_text)
  return conf != new_text


def DescendDirectory(input_dirname: str,
                     ignore_directories: List[str]) -> List[pathlib.Path]:
  """Descend from input_dirname looking for policy files to render.

  Args:
    input_dirname: the base directory.
    ignore_directories: directories to ignore while traversing.

  Returns:
    a list of input file paths
  """
  input_dir = pathlib.Path(input_dirname)

  policy_files: List[pathlib.Path] = []
  policy_directories: Iterator[pathlib.Path] = filter(
      lambda path: path.is_dir(), input_dir.glob('**/pol'))
  for ignored_directory in ignore_directories:

    def Filtering(path, ignored=ignored_directory):
      return not path.match('%s/**/pol' % ignored) and not path.match(
          '%s/pol' % ignored)

    policy_directories = filter(Filtering, policy_directories)

  for directory in policy_directories:
    directory_policies = list(directory.glob('*.pol'))
    depth = len(directory.parents) - 1
    logging.warning('-' * (2 * depth) + '> %s (%d pol files found)' %
                    (directory, len(directory_policies)))
    policy_files.extend(filter(lambda path: path.is_file(), directory_policies))

  return policy_files


def WriteFiles(write_files: WriteList):
  """Writes files to disk.

  Args:
    write_files: List of file names and strings.
  """
  if write_files:
    logging.info('writing %d files to disk...', len(write_files))
  else:
    logging.info('no files changed, not writing to disk')
  for output_file, file_contents in write_files:
    _WriteFile(output_file, file_contents)


def _WriteFile(output_file: pathlib.Path, file_contents: str):
  """Inner file writing function.

  Args:
    output_file: Path to write to
    file_contents: Data to write
  """
  try:
    parent_path = pathlib.Path(output_file).parent
    if not parent_path.is_dir():
      parent_path.mkdir(parents=True, exist_ok=True)
    with open(output_file, 'w') as output:
      logging.info('writing file: %s', output_file)
      output.write(file_contents)
  except IOError:
    logging.warning('error while writing file: %s', output_file)
    raise


def Run(base_directory: str, definitions_directory: str, policy_file: str,
        output_directory: str, exp_info: int, max_renderers: int,
        ignore_directories: List[str], optimize: bool, shade_check: bool,
        context: multiprocessing.context.BaseContext):
  """Generate ACLs.

  Args:
    base_directory: directory containing policy files.
    definitions_directory: directory containing NETWORK and SERVICES definition
      files.
    policy_file: path to a single policy file to render.
    output_directory: directory in which rendered files are placed.
    exp_info: print a info message when a term is set to expire in that many
      weeks.
    max_renderers: the number of renderers to run in parallel.
    ignore_directories: directories to ignore when searching for policy files.
    optimize: a boolean indicating if we should turn on optimization or not.
    shade_check: should we raise an error if a term is completely shaded.
    context: multiprocessing context
  """
  definitions = None
  try:
    definitions = naming.Naming(definitions_directory)
  except naming.NoDefinitionsError:
    err_msg = 'bad definitions directory: %s' % definitions_directory
    logging.fatal(err_msg)
    return  # static type analyzer can't detect that logging.fatal exits program

  # thead-safe list for storing files to write
  manager: multiprocessing.managers.SyncManager = context.Manager()
  write_files: WriteList = manager.list()

  with_errors = False
  logging.info('finding policies...')
  if policy_file:
    # render just one file
    logging.info('rendering one file')
    RenderFile(base_directory, pathlib.Path(policy_file),
               pathlib.Path(output_directory), definitions, exp_info, optimize,
               shade_check, write_files)
  elif max_renderers == 1:
    # If only one process, run it sequentially
    policies = DescendDirectory(base_directory, ignore_directories)
    for pol in policies:
      RenderFile(base_directory, pol, pathlib.Path(output_directory),
                 definitions, exp_info, optimize, shade_check, write_files)
  else:
    # render all files in parallel
    policies = DescendDirectory(base_directory, ignore_directories)
    pool = context.Pool(processes=max_renderers)
    results: List[multiprocessing.pool.AsyncResult] = []
    for pol in policies:
      results.append(
          pool.apply_async(
              RenderFile,
              args=(base_directory, pol, output_directory, definitions,
                    exp_info, optimize, shade_check, write_files)))
    pool.close()
    pool.join()

    for result in results:
      try:
        result.get()
      except (ACLParserError, ACLGeneratorError) as e:
        with_errors = True
        logging.warning('\n\nerror encountered in rendering process:\n%s\n\n',
                        e)

  # actually write files to disk
  WriteFiles(write_files)

  if with_errors:
    logging.warning('done, with errors.')
    sys.exit(1)
  else:
    logging.info('done.')


def main(argv):
  del argv  # Unused.

  configs = config.generate_configs(FLAGS)

  if configs['verbose']:
    logging.set_verbosity(logging.INFO)
  if configs['debug']:
    logging.set_verbosity(logging.DEBUG)
  logging.debug(
      'binary: %s\noptimize: %d\nbase_directory: %s\n'
      'policy_file: %s\nrendered_acl_directory: %s', str(sys.argv[0]),
      int(configs['optimize']), str(configs['base_directory']),
      str(configs['policy_file']), str(configs['output_directory']))
  logging.debug('capirca configurations: %s', configs)

  context = multiprocessing.get_context()

  Run(configs['base_directory'], configs['definitions_directory'],
      configs['policy_file'], configs['output_directory'], configs['exp_info'],
      configs['max_renderers'], configs['ignore_directories'],
      configs['optimize'], configs['shade_check'], context)


def EntryPoint():
  """Read in flags and call main()."""
  SetupFlags()
  app.run(main)


if __name__ == '__main__':
  EntryPoint()


# capirca/lib/__init__.py

"""Libraries for Capirca."""


# capirca/lib/aclcheck.py

# Copyright 2011 Google 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.
#

"""Check where hosts, ports and protocols are matched in a capirca policy."""

import logging
from capirca.lib import nacaddr
from capirca.lib import policy
from capirca.lib import port


class Error(Exception):
  """Base error class."""


class AddressError(Error):
  """Incorrect IP address or format."""


class BadPolicyError(Error):
  """Item is not a valid policy object."""


class NoTargetError(Error):
  """Specified target platform not available in specified policy."""


class AclCheck:
  """Check where hosts, ports and protocols match in a NAC policy.

  Attributes:
    pol_obj: policy.Policy object.
    pol: policy.Policy object.
    src: A string for the source address.
    dst: A string for the destination address.
    sport: A string for the source port.
    dport: A string for the destination port.
    proto: A string for the protocol.
    matches: A list of term-related matches.
    exact_matches: A list of exact matches.

  Returns:
    An AclCheck Object

  Raises:
    port.BarPortValue: An invalid source port is used
    port.BadPortRange: A port is outside of the acceptable range 0-65535
    AddressError: Incorrect ip address or format

  """

  def __init__(self,
               pol,
               src='any',
               dst='any',
               sport='any',
               dport='any',
               proto='any',
              ):

    logging.debug('aclcheck __init__')
    self.pol_obj = pol
    self.proto = proto

    # validate source port
    if sport == 'any':
      self.sport = sport
    else:
      self.sport = port.Port(sport)

    # validate destination port
    if dport == 'any':
      self.dport = dport
    else:
      self.dport = port.Port(dport)

    # validate source address
    if src == 'any':
      self.src = src
    else:
      try:
        self.src = nacaddr.IP(src)
      except ValueError:
        raise AddressError('bad source address: %s\n' % src)

    # validate destination address
    if dst == 'any':
      self.dst = dst
    else:
      try:
        self.dst = nacaddr.IP(dst)
      except ValueError:
        raise AddressError('bad destination address: %s\n' % dst)

    if not isinstance(self.pol_obj, (policy.Policy)):
      raise BadPolicyError('Policy object is not valid.')

    self.matches = []
    self.exact_matches = []
    for header, terms in self.pol_obj.filters:
      filtername = header.target[0].options[0]
      for term in terms:
        possible = []
        logging.debug('checking term: %s', term.name)
        if not self._AddrInside(self.src, term.source_address):
          logging.debug('srcaddr does not match')
          continue
        logging.debug('srcaddr matches: %s', self.src)
        if not self._AddrInside(self.dst, term.destination_address):
          logging.debug('dstaddr does not match')
          continue
        logging.debug('dstaddr matches: %s', self.dst)
        if (self.sport != 'any' and term.source_port and not
            self._PortInside(self.sport, term.source_port)):
          logging.debug('sport does not match')
          continue
        logging.debug('sport matches: %s', self.sport)
        if (self.dport != 'any' and term.destination_port and not
            self._PortInside(self.dport, term.destination_port)):
          logging.debug('dport does not match')
          continue
        logging.debug('dport matches: %s', self.dport)
        if (self.proto != 'any' and term.protocol and
            self.proto not in term.protocol):
          logging.debug('proto does not match')
          continue
        logging.debug('proto matches: %s', self.proto)
        if term.protocol_except and self.proto in term.protocol_except:
          logging.debug('protocol excepted by term, no match.')
          continue
        logging.debug('proto not excepted: %s', self.proto)
        if not term.action:  # avoid any verbatim
          logging.debug('term had no action (verbatim?), no match.')
          continue
        logging.debug('term has an action')
        possible = self._PossibleMatch(term)
        self.matches.append(Match(filtername, term.name, possible, term.action,
                                  term.qos))
        if possible:
          logging.debug('term has options: %s, not treating as exact match',
                        possible)
          continue

        # if we get here then we have a match, and if the action isn't next and
        # there are no possibles, then this is a "definite" match and we needn't
        # look for any further matches (i.e. later terms may match, but since
        # we'll never get there we shouldn't report them)
        if 'next' not in term.action:
          self.exact_matches.append(Match(filtername, term.name, [],
                                          term.action, term.qos))
          break

  def Matches(self):
    """Return list of matched terms."""
    return self.matches

  def ExactMatches(self):
    """Return matched terms, but not terms with possibles or action next."""
    return self.exact_matches

  def ActionMatch(self, action='any'):
    """Return list of matched terms with specified actions."""
    match_list = []
    for match in self.matches:
      if match.action:
        if not match.possibles:
          if action == 'any' or action in match.action:
            match_list.append(match)
    return match_list

  def DescribeMatches(self):
    """Provide sentence descriptions of matches.

    Returns:
      ret_str: text sentences describing matches
    """
    ret_str = []
    for match in self.matches:
      text = str(match)
      ret_str.append(text)
    return '\n'.join(ret_str)

  def __str__(self):
    text = []
    last_filter = ''
    for match in self.matches:
      if match.filter != last_filter:
        last_filter = match.filter
        text.append('  filter: ' + match.filter)
      if match.possibles:
        text.append(' ' * 10 + 'term: ' + str(match.term) + ' (possible match)')
      else:
        text.append(' ' * 10 + 'term: ' + str(match.term))
      if match.possibles:
        text.append(' ' * 16 + match.action + ' if ' + str(match.possibles))
      else:
        text.append(' ' * 16 + match.action)
    return '\n'.join(text)

  def _PossibleMatch(self, term):
    """Ignore some options and keywords that are edge cases.

    Args:
      term: term object to examine for edge-cases

    Returns:
      ret_str: a list of reasons this term may possible match
    """
    ret_str = []
    if 'first-fragment' in term.option:
      ret_str.append('first-frag')
    if term.fragment_offset:
      ret_str.append('frag-offset')
    if term.packet_length:
      ret_str.append('packet-length')
    if 'established' in term.option:
      ret_str.append('est')
    if 'tcp-established' in term.option and 'tcp' in term.protocol:
      ret_str.append('tcp-est')
    return ret_str

  def _AddrInside(self, addr, addresses):
    """Check if address is matched in another address or group of addresses.

    Args:
      addr: An ipaddr network or host address or text 'any'
      addresses: A list of ipaddr network or host addresses

    Returns:
      bool: True of false
    """
    if addr == 'any': return True   # always true if we match for any addr
    if not addresses: return True   # always true if term has nothing to match
    for ip in addresses:
      # ipaddr can incorrectly report ipv4 as contained with ipv6 addrs
      if addr.subnet_of(ip):
        return True
    return False

  def _PortInside(self, myport, port_list):
    """Check if port matches in a port or group of ports.

    Args:
      myport: port number
      port_list: list of ports

    Returns:
      bool: True of false
    """
    if myport == 'any': return True
    if [x for x in port_list if x[0] <= myport <= x[1]]:
      return True
    return False


class Match:
  """A matching term and its associate values."""

  def __init__(self, filtername, term, possibles, action, qos=None):
    self.filter = filtername
    self.term = term
    self.possibles = possibles
    self.action = action[0]
    self.qos = qos

  def __str__(self):
    text = ''
    if self.possibles:
      text += 'possible ' + self.action
    else:
      text += self.action
    text += ' in term ' + self.term + ' of filter ' + self.filter
    if self.possibles:
      text += ' with factors: ' + str(', '.join(self.possibles))
    return text


def main():
  pass

if __name__ == '__main__':
  main()


# capirca/lib/aclgenerator.py

# Copyright 2011 Google 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.
#

"""ACL Generator base class."""

import copy
import logging
import re
import string

from capirca.lib import policy
import six


# generic error class
class Error(Exception):
  """Base error class."""


class NoPlatformPolicyError(Error):
  """Raised when a policy is received that doesn't support this platform."""


class UnknownIcmpTypeError(Error):
  """Raised when we see an unknown icmp-type."""


class MismatchIcmpInetError(Error):
  """Raised when mistmatch between icmp/icmpv6 and inet/inet6."""


class EstablishedError(Error):
  """Raised when a term has established option with inappropriate protocol."""


class UnsupportedAFError(Error):
  """Raised when provided an unsupported address family."""


class DuplicateTermError(Error):
  """Raised when duplication of term names are detected."""


class UnsupportedFilterError(Error):
  """Raised when we see an inappropriate filter."""


class UnsupportedTargetOptionError(Error):
  """Raised when a filter has an impermissible default action specified."""


class TermNameTooLongError(Error):
  """Raised when term named can not be abbreviated."""


class Term:
  """Generic framework for a generator Term."""
  ICMP_TYPE = policy.Term.ICMP_TYPE
  # http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
  PROTO_MAP = {'hopopt': 0,
               'icmp': 1,
               'igmp': 2,
               'ggp': 3,
               'ipip': 4,
               'tcp': 6,
               'egp': 8,
               'igp': 9,
               'udp': 17,
               'rdp': 27,
               'ipv6': 41,
               'ipv6-route': 43,
               'fragment': 44,
               'rsvp': 46,
               'gre': 47,
               'esp': 50,
               'ah': 51,
               'icmpv6': 58,
               'ipv6-nonxt': 59,
               'ipv6-opts': 60,
               'ospf': 89,
               'pim': 103,
               'vrrp': 112,
               'l2tp': 115,
               'sctp': 132,
               'udplite': 136,
               'all': -1,  # Used for GCE default deny, do not use in pol file.
              }
  AF_MAP = {'inet': 4,
            'inet6': 6,
            'bridge': 4  # if this doesn't exist, output includes v4 & v6
           }
  # These protos are always expressed as numbers instead of name
  #  due to inconsistencies on the end platform's name-to-number
  #  mapping.
  ALWAYS_PROTO_NUM = ['ipip']
  # provide flipped key/value dicts
  PROTO_MAP_BY_NUMBER = dict([(v, k) for (k, v) in PROTO_MAP.items()])
  AF_MAP_BY_NUMBER = dict([(v, k) for (k, v) in AF_MAP.items()])

  NO_AF_LOG_ADDR = string.Template('Term $term will not be rendered, as it has'
                                   ' $direction address match specified but no'
                                   ' $direction addresses of $af address family'
                                   ' are present.')

  NO_AF_LOG_PROTO = string.Template('Term $term will not be rendered, as it has'
                                    ' $proto match specified but the ACL is of'
                                    ' $af address family.')

  def __init__(self, term):
    if term.protocol:
      for protocol in term.protocol:
        if (protocol not in self.PROTO_MAP and
            str(protocol) not in [str(p) for p in self.PROTO_MAP_BY_NUMBER]):
          raise UnsupportedFilterError('Protocol(s) %s are not supported.'
                                       % str(term.protocol))

      term.protocol = ProtocolNameToNumber(term.protocol,
                                           self.ALWAYS_PROTO_NUM,
                                           self.PROTO_MAP)
    self.term = term

  def NormalizeAddressFamily(self, af):
    """Convert (if necessary) address family name to numeric value.

    Args:
      af: Address family, can be either numeric or string (e.g. 4 or 'inet')

    Returns:
      af: Numeric address family value

    Raises:
      UnsupportedAFError: Address family not in keys or values of our AF_MAP.
    """
    # ensure address family (af) is valid
    if af in self.AF_MAP_BY_NUMBER:
      return af
    elif af in self.AF_MAP:
      # convert AF name to number (e.g. 'inet' becomes 4, 'inet6' becomes 6)
      af = self.AF_MAP[af]
    else:
      raise UnsupportedAFError('Address family %s is not supported, '
                               'term %s.' % (af, self.term.name))
    return af

  def NormalizeIcmpTypes(self, icmp_types, protocols, af):
    """Return verified list of appropriate icmp-types.

    Args:
      icmp_types: list of icmp_types
      protocols: list of protocols
      af: address family of this term, either numeric or text (see self.AF_MAP)

    Returns:
      sorted list of numeric icmp-type codes.

    Raises:
      UnsupportedFilterError: icmp-types specified with non-icmp protocol.
      MismatchIcmpInetError: mismatch between icmp protocol and address family.
      UnknownIcmpTypeError: unknown icmp-type specified
    """
    if not icmp_types:
      return ['']
    # only protocols icmp or icmpv6 can be used with icmp-types
    if protocols != ['icmp'] and protocols != ['icmpv6']:
      raise UnsupportedFilterError('%s %s' % (
          'icmp-types specified for non-icmp protocols in term: ',
          self.term.name))
    # make sure we have a numeric address family (4 or 6)
    af = self.NormalizeAddressFamily(af)
    # check that addr family and protocl are appropriate
    if ((af != 4 and protocols == ['icmp']) or
        (af != 6 and protocols == ['icmpv6'])):
      raise MismatchIcmpInetError('%s %s, %s: %s, %s: %s' % (
          'ICMP/ICMPv6 mismatch with address family IPv4/IPv6 in term',
          self.term.name, 'address family', af, 'protocols',
          ','.join(protocols)))
    # ensure all icmp types are valid
    for icmptype in icmp_types:
      if icmptype not in self.ICMP_TYPE[af]:
        raise UnknownIcmpTypeError('%s %s %s %s' % (
            '\nUnrecognized ICMP-type (', icmptype,
            ') specified in term ', self.term.name))
    rval = []
    rval.extend([self.ICMP_TYPE[af][x] for x in icmp_types])
    rval.sort()
    return rval


class ACLGenerator:
  """Generates platform specific filters and terms from a policy object.

  This class takes a policy object and renders the output into a syntax which
  is understood by a specific platform (eg. iptables, cisco, etc).
  """

  _PLATFORM = None
  # Default protocol to apply when no protocol is specified.
  _DEFAULT_PROTOCOL = 'ip'
  # Unsupported protocols by address family.
  _SUPPORTED_AF = {'inet', 'inet6'}
  # Commonly misspelled protocols that the generator should reject.
  _FILTER_BLACKLIST = {}

  # Only warn if these tokens are not implemented by a platform. These are not
  # meant to be overridden in subclasses like supported tokens/sub tokens.
  WARN_IF_UNSUPPORTED = {
      'restrict_address_family',
      'counter',
      'destination_tag',
      'filter_term',
      'logging',
      'loss_priority',
      'owner',
      'qos',
      'routing_instance',
      'policer',
      'source_tag'
  }

  # Abbreviation table used to automatically abbreviate terms that exceed
  # specified limit. We use uppercase for abbreviations to distinguish
  # from lowercase names.  This is order list - we try the ones in the
  # top of the list before the ones later in the list.  Prefer clear
  # or very-space-saving abbreviations by putting them early in the
  # list.  Abbreviations may be regular expressions or fixed terms;
  # prefer fixed terms unless there's a clear benefit to regular
  # expressions.
  _ABBREVIATION_TABLE = [
      # Service abbreviations first.
      ('experiment', 'EXP'),
      ('wifi-radius', 'W-R'),
      ('customer', 'CUST'),
      ('server', 'SRV'),
      # Next, common routing terms
      ('global', 'GBL'),
      ('google', 'GOOG'),
      ('service', 'SVC'),
      ('router', 'RTR'),
      ('internal', 'INT'),
      ('external', 'EXT'),
      ('transit', 'TRNS'),
      ('management', 'MGMT'),
      # State info
      ('established', 'EST'),
      ('unreachable', 'UNR'),
      ('fragment', 'FRAG'),
      ('accept', 'ACC'),
      ('discard', 'DISC'),
      ('reject', 'REJ'),
      ('replies', 'RPL'),
      ('request', 'REQ'),
  ]
  # Maximum term length. Can be overridden by generator to enforce
  # platform specific restrictions.
  _TERM_MAX_LENGTH = 62

  def __init__(self, pol, exp_info):
    """Initialise an ACLGenerator.  Store policy structure for processing."""
    supported_tokens, supported_sub_tokens = self._GetSupportedTokens()

    self.policy = pol
    all_err = []
    all_warn = []
    for header, terms in pol.filters:
      if self._PLATFORM in header.platforms:
        # Verify valid keywords
        # error on unsupported optional keywords that could result
        # in dangerous or unexpected results
        for term in terms:
          if term.platform:
            if self._PLATFORM not in term.platform:
              continue
          if term.platform_exclude:
            if self._PLATFORM in term.platform_exclude:
              continue
          # Only verify optional keywords if the term is active on the platform.
          err = []
          warn = []
          for el, val in term.__dict__.items():
            # Private attributes do not need to be valid keywords.
            if (val and el not in supported_tokens and not
                el.startswith('flatten')):
              if val and el not in self.WARN_IF_UNSUPPORTED:
                err.append(el)
              else:
                warn.append(el)
            # ignore Liskov's rule.
            if (val and isinstance(val, list) and
                el in supported_sub_tokens):
              ns = set(val) - supported_sub_tokens[el]
              # hack support for ArbitraryOptions in junos. todo, add the
              # junos options into the lexer, then we can nuke .*
              # shenanigans.
              if ns and '.*' not in supported_sub_tokens[el]:
                err.append(' '.join(ns))
          if err:
            all_err.append(('%s contains unsupported keywords (%s) for target '
                            '%s in policy %s') % (term.name, ' '.join(err),
                                                  self._PLATFORM, pol.filename))
          if warn:
            all_warn.append(
                ('%s contains unimplemented keywords (%s) for '
                 'target %s in policy %s') % (term.name, ' '.join(warn),
                                              self._PLATFORM, pol.filename))
        continue
    if all_err:
      raise UnsupportedFilterError('\n %s' % '\n'.join(all_err))
    if all_warn:
      logging.debug('\n %s', '\n'.join(all_warn))
    self._TranslatePolicy(pol, exp_info)

  def _TranslatePolicy(self, pol, exp_info):
    # pylint: disable=unused-argument
    """Translate policy contents to platform specific data structures."""
    raise Error('%s does not implement _TranslatePolicies()' % self._PLATFORM)

  def _BuildTokens(self):
    """Provide a default for supported tokens and sub tokens.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    # Set of supported keywords for a given platform.  Values should be in
    # undercase form, eg, icmp_type (not icmp-type)
    supported_tokens = {'action',
                        'comment',
                        'destination_address',
                        'destination_address_exclude',
                        'destination_port',
                        'expiration',
                        'icmp_type',
                        'stateless_reply',
                        'name',  # obj attribute, not token
                        'option',
                        'protocol',
                        'platform',
                        'platform_exclude',
                        'source_address',
                        'source_address_exclude',
                        'source_port',
                        'translated',  # obj attribute, not token
                        'verbatim',
                        }

    # These keys must be also listed in supported_tokens.
    # Keys should be in undercase form, eg, icmp_type (not icmp-type). Values
    # should be in dash form, icmp-type (not icmp_type)
    supported_sub_tokens = {
        'option': {
            'established',
            'first-fragment',
            'is-fragment',
            'initial',
            'rst',
            'sample',
            'tcp-established',
        },
        'action': {
            'accept',
            'deny',
            'next',
            'reject',
            'reject-with-tcp-rst',
        },
        'icmp_type': set(list(Term.ICMP_TYPE[4].keys())
                         + list(Term.ICMP_TYPE[6].keys()))
    }
    return supported_tokens, supported_sub_tokens

  def _GetSupportedTokens(self):
    """Build our supported tokens and sub tokens.

    Returns:
      tuple containing the supported tokens and sub tokens.
    Raises:
      UnsupportedFilterError: Raised when token is not supported.
    """
    supported_tokens, supported_sub_tokens = self._BuildTokens()
    # make sure we don't have subtokens that are not listed. This should not
    # occur unless a platform's tokens/subtokens are changed.
    undefined_st = set(supported_sub_tokens) - supported_tokens
    if undefined_st:
      raise UnsupportedFilterError(
          'Found undefined sub tokens missing from the supported token list! '
          'These must match. (%s)' % ' '.join(undefined_st))
    # all good.
    return supported_tokens, supported_sub_tokens

  # TODO(robankeny) Fix this function, it no longer does what it says.
  def FixHighPorts(self, term, af='inet', all_protocols_stateful=False):
    """Evaluate protocol and ports of term, return sane version of term.

    Args:
      term: Term object to be checked
      af: String presenting the address family, inet, inet6
      all_protocols_stateful: Boolean suggesting if protocols are all stateful.

    Returns:
      Copy of term that has been fixed

    Raises:
      UnsupportedAFError: Address family provided but unsupported.
      UnsupportedFilter: Protocols do not match the address family.
      EstablishedError: Established option used with inappropriate protocol.
      UnsupportedFilterError: Filter does not support protocols with AF.
    """
    mod = term

    # Determine which protocols this term applies to.
    if term.protocol:
      protocols = set(term.protocol)
    else:
      protocols = set((self._DEFAULT_PROTOCOL,))

    # Check that the address family matches the protocols.
    if af not in self._SUPPORTED_AF:
      raise UnsupportedAFError(
          '\nAddress family %s, found in %s, unsupported '
          'by %s' % (af, term.name, self._PLATFORM))
    if af in self._FILTER_BLACKLIST:
      unsupported_protocols = self._FILTER_BLACKLIST[af].intersection(protocols)
      if unsupported_protocols:
        raise UnsupportedFilterError(
            '\n%s targets do not support protocol(s) %s '
            'with address family %s (in %s)' % (self._PLATFORM,
                                                unsupported_protocols,
                                                af,
                                                term.name))

    # Many renders expect high ports for terms with the established option.
    for opt in [str(x) for x in term.option]:
      if opt.find('established') == 0:
        unstateful_protocols = protocols.difference(set(('tcp', 'udp')))
        if not unstateful_protocols:
          # TCP/UDP: add in high ports then collapse to eliminate overlaps.
          mod = copy.deepcopy(term)
          if not all_protocols_stateful:
            mod.destination_port.append((1024, 65535))
          mod.destination_port = mod.CollapsePortList(mod.destination_port)
        elif not all_protocols_stateful:
          errmsg = 'Established option supplied with inappropriate protocol(s)'
          raise EstablishedError('%s %s %s %s' %
                                 (errmsg, unstateful_protocols,
                                  'in term', term.name))
        break

    return mod

  def FixTermLength(self, term_name, abbreviate=False, truncate=False):
    """Return a term name which is equal or shorter than _TERM_MAX_LENGTH.

       New term is obtained in two steps. First, if allowed, automatic
       abbreviation is performed using hardcoded abbreviation table. Second,
       if allowed, term name is truncated to specified limit.

    Args:
      term_name: Name to abbreviate if necessary.
      abbreviate: Whether to allow abbreviations to shorten the length.
      truncate: Whether to allow truncation to shorten the length.
    Returns:
       A string based on term_name, that is equal or shorter than
       _TERM_MAX_LENGTH abbreviated and truncated as necessary.
    Raises:
       TermNameTooLongError: term_name cannot be abbreviated
       to be shorter than _TERM_MAX_LENGTH, or truncation is disabled.
    """
    new_term = term_name
    if abbreviate:
      for word, abbrev in self._ABBREVIATION_TABLE:
        if len(new_term) <= self._TERM_MAX_LENGTH:
          return new_term
        new_term = re.sub(word, abbrev, new_term)
    if truncate:
      new_term = new_term[:self._TERM_MAX_LENGTH]
    if len(new_term) <= self._TERM_MAX_LENGTH:
      return new_term
    raise TermNameTooLongError('Term %s (originally %s) is '
                               'too long. Limit is %d characters (vs. %d) '
                               'and no abbreviations remain or abbreviations '
                               'disabled.' %
                               (new_term, term_name,
                                self._TERM_MAX_LENGTH,
                                len(new_term)))


def ProtocolNameToNumber(protocols, proto_to_num, name_to_num_map):
  """Convert a protocol name to a numeric value.

  Args:
    protocols: list of protocol names to inspect
    proto_to_num: list of protocol names that should be converted to numbers
    name_to_num_map: map of protocol names to protocol numbers

  Returns:
    return_proto: list of protocol names, converted if applicable
  """
  return_proto = []

  for protocol in protocols:
    if protocol in proto_to_num:
      return_proto.append(name_to_num_map[protocol])
    else:
      return_proto.append(protocol)

  return return_proto


def AddRepositoryTags(prefix='', rid=True, date=True, revision=True,
                      wrap=False):
  """Add repository tagging into the output.

  Args:
    prefix: comment delimiter, if needed, to appear before tags
    rid: bool; True includes the revision Id: repository tag.
    date: bool; True includes the Date: repository tag.
    revision: bool; True includes the Revision: repository tag.
    wrap: bool; True wraps the tag in double quotes.
  Returns:
    list of text lines containing revision data
  """
  tags = []
  wrapper = '"' if wrap else ''

  # Format print the '$' into the RCS tags in order prevent the tags from
  # being interpolated here.
  p4_id = '%s%sId:%s%s' % (wrapper, '$', '$', wrapper)
  p4_date = '%s%sDate:%s%s' % (wrapper, '$', '$', wrapper)
  p4_revision = '%s%sRevision:%s%s' % (wrapper, '$', '$', wrapper)
  if rid:
    tags.append('%s%s' % (prefix, p4_id))
  if date:
    tags.append('%s%s' % (prefix, p4_date))
  if revision:
    tags.append('%s%s' % (prefix, p4_revision))
  return tags


def WrapWords(textlist, size, joiner='\n'):
  r"""Insert breaks into the listed strings at specified width.

  Args:
    textlist: a list of text strings
    size: width of reformated strings
    joiner: text to insert at break.  eg. '\n  ' to add an indent.
  Returns:
    list of strings
  """
  # \S{%d}(?!\s|\Z) collets the max size for words that are larger than the max
  # (?<=\S{%d})\S+ collects the remaining text for overflow words in their own
  # line
  # \S.{1,%d}(?=\s|\Z)) collects all words and spaces up to max size, breaking
  # at the last space
  rval = []
  linelength_re = re.compile(
      r'(\S{%d}(?!\s|\Z)|(?<=\S{%d})\S+|\S.{1,%d}(?=\s|\Z))' %
      (size, size, size - 1))
  for index in range(len(textlist)):
    if len(textlist[index]) > size:
      # insert joiner into the string at appropriate places.
      textlist[index] = joiner.join(linelength_re.findall(textlist[index]))
    # avoid empty comment lines
    rval.extend(x.strip() for x in textlist[index].strip().split(joiner) if x)
  return rval


# capirca/lib/arista.py

# Copyright 2015 Google 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.
#

"""Arista generator."""

from capirca.lib import cisco


class Error(Exception):
  """Base error class."""


class UnsupportedEosAccessListError(Error):
  """When a filter type is not supported in an EOS policy target."""


class Arista(cisco.Cisco):
  """An Arista policy object.

  EOS devices differ slightly from Cisco, omitting the extended argument to
  ACLs for example. There are other items such as 'tracked' we may want to add
  in the future.
  """

  _PLATFORM = 'arista'
  SUFFIX = '.eacl'
  # Protocols should be emitted as they were in the policy (names).
  _PROTO_INT = False

  # Arista omits the "extended" access-list argument.
  def _AppendTargetByFilterType(self, filter_name, filter_type):
    """Takes in the filter name and type and appends headers.

    Args:
      filter_name: Name of the current filter
      filter_type: Type of current filter

    Returns:
      list of strings

    Raises:
      UnsupportedEosAccessListError: When unknown filter type is used.
    """
    target = []
    if filter_type == 'standard':
      if filter_name.isdigit():
        target.append('no access-list %s' % filter_name)
      else:
        target.append('no ip access-list standard %s' % filter_name)
        target.append('ip access-list standard %s' % filter_name)
    elif filter_type == 'extended':
      target.append('no ip access-list %s' % filter_name)
      target.append('ip access-list %s' % filter_name)
    elif filter_type == 'object-group':
      target.append('no ip access-list %s' % filter_name)
      target.append('ip access-list %s' % filter_name)
    elif filter_type == 'inet6':
      target.append('no ipv6 access-list %s' % filter_name)
      target.append('ipv6 access-list %s' % filter_name)
    else:
      raise UnsupportedEosAccessListError(
          'access list type %s not supported by %s' % (
              filter_type, self._PLATFORM))
    return target


# capirca/lib/arista_tp.py

# Copyright 2021 Google 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.
#
"""arista traffic-policy generator."""

import copy
import datetime
import re
import textwrap

from absl import logging
from capirca.lib import aclgenerator
import six

#          1         2         3
# 123456789012345678901234567890123456789
# traffic-policies
#    traffic-policy foo
#      match dos-attaqrs-source-ip ipv4    << TERM_INDENT
#                  1         2         3         4         5         6
#         123456789012345678901234567890123456789012345678901234567890123456789
#         !! i am a comment, hear me rawr  << MATCH_INDENT
#         !!
#         source prefix field-set          << MATCH_INDENT
#         !
#         actions
#            counter edge-dos-attaqrs-source-ip-count  << ACTION_INDENT
#            drop
#      !
#
#          1         2         3
# 123456789012345678901234567890123456789
# traffic-policies
#    field-set ipv4 prefix dst-hjjqurby6yftqk6fa3xx4fas << TERM_INDENT
#       0.0.0.0/0                                       << MATCH_INDENT
#       except 34.64.0.0/26
#    !
#    field-set ipv4 prefix dst-hjjqurby6yftqk6fa3xx4fas

# various indentation constants - see above
INDENT_STR = " " * 3  # 3 spaces
TERM_INDENT = 2 * INDENT_STR
MATCH_INDENT = 3 * INDENT_STR
ACTION_INDENT = 4 * INDENT_STR
MAX_COMMENT_LENGTH = 60

# generic error class


class Error(Exception):
  pass


class TcpEstablishedWithNonTcpError(Error):
  pass


class AristaTpFragmentInV6Error(Error):
  pass


class Config:
  """config allows a configuration to be assembled easily.

  when appending to the configuration object, the element should be indented
  according to the arista traffic-policy style.

  a text representation of the config can be extracted with str().

  attributes:
  indent: The number of leading spaces on the current line.
  lines: the text lines of the configuration.

  """

  def __init__(self):
    self.lines = []

  def __str__(self):
    return "\n".join(self.lines)

  def Append(self, line_indent, line, verbatim=False):
    """append one line to the configuration.

    Args:
      line_indent: config specific spaces prepended to the line
      line: the configuratoin string to append to the config.
      verbatim: append line without adjusting indentation. Default False.
    """
    if verbatim:
      self.lines.append(line)
      return

    self.lines.append(line_indent + line.strip())


class Term(aclgenerator.Term):
  """represents an individual AristaTrafficPolicy term.

  useful for the __str__() method. where literally, everything interesting
  happens.

  attributes:
   term: the term object from the policy.
   term_type: string indicating type of term, inet, inet6 etc.
   noverbose: boolean to disable verbosity.

  """

  _PLATFORM = "arista_tp"
  _ACTIONS = {
      "accept": "",
      "deny": "drop",
      "reject": "drop",  # unsupported action, convert to drop
      "reject-with-tcp-rst": "drop",  # ibid
      # "next": "continue",
  }
  AF_MAP = {
      "inet": 4,
      "inet6": 6,
  }
  # the following lookup table is used to map between the various types of
  # filters the generator can render.  as new differences are
  # encountered, they should be added to this table.  Accessing members
  # of this table looks like:
  #  self._TERM_TYPE('inet').get('saddr') -> 'source-address'
  #
  # it's critical that the members of each filter type be the same, that is
  # to say that if _TERM_TYPE.get('inet').get('foo') returns something,
  # _TERM_TYPE.get('inet6').get('foo') must return the inet6 equivalent.
  _TERM_TYPE = {
      "inet": {
          "addr_fam": "ipv4",
      },
      "inet6": {
          "addr_fam": "ipv6",
      },
  }

  def __init__(self, term, term_type, noverbose):
    super().__init__(term)
    self.term = term
    self.term_type = term_type  # drives the address-family
    self.noverbose = noverbose

    if term_type not in self._TERM_TYPE:
      raise ValueError("unknown filter type: %s" % term_type)

  def __str__(self):
    # verify platform specific terms. skip the whole term if the platform
    # does not match.
    if (self.term.platform and self._PLATFORM not in self.term.platform):
      return ""
    if (self.term.platform_exclude and
        self._PLATFORM in self.term.platform_exclude):
      return ""

    config = Config()

    # a LoL which will be appended to the config at the end of this method
    # elements will be of the form [indentation, string, verbatim] by
    # default verbatim = False
    term_block = []

    # don't render icmpv6 protocol terms under inet, or icmp under inet6
    if (self.term_type == "inet6" and
        "icmp" in self.term.protocol) or (self.term_type == "inet" and
                                          "icmpv6" in self.term.protocol):
      logging.debug(
          self.NO_AF_LOG_PROTO.substitute(
              term=self.term.name,
              proto=", ".join(self.term.protocol),
              af=self.term_type,
          ))
      return ""

    # term verbatim output - this will skip over normal term creation
    # code.  warning generated from policy.py if appropriate.
    if self.term.verbatim:
      for line in self.term.verbatim:
        if line[0] == self._PLATFORM:
          # pass MATCH_INDENT, but this should be ignored in the
          # rendering
          term_block.append([MATCH_INDENT, str(line[1]), True])
      # we return immediately, there's no action to be formed
      for i, s, v in term_block:
        config.Append(i, s, verbatim=v)

      return str(config)

    # option processing
    flags = []
    misc_options = []
    if self.term.option:
      flags, misc_options = self._processTermOptions(self.term,
                                                     self.term.option)

    # helper for per-address-family keywords.
    family_keywords = self._TERM_TYPE.get(self.term_type)

    term_block.append([
        TERM_INDENT,
        "match %s %s" % (self.term.name, family_keywords["addr_fam"]), False
    ])

    term_af = self.AF_MAP.get(self.term_type)
    if self.term.owner and not self.noverbose:
      self.term.comment.append("owner: %s" % self.term.owner)
    if self.term.comment and not self.noverbose:
      reflowed_comments = self._reflowComments(self.term.comment,
                                               MAX_COMMENT_LENGTH)
      for line in reflowed_comments:
        term_block.append([MATCH_INDENT, "!! " + line, False])

    has_match_criteria = (
        self.term.destination_address or
        self.term.destination_address_exclude or self.term.destination_port or
        self.term.destination_prefix or self.term.fragment_offset or
        self.term.hop_limit or self.term.port or self.term.protocol or
        self.term.protocol_except or self.term.source_address or
        self.term.source_address_exclude or self.term.source_port or
        self.term.source_prefix or self.term.ttl)

    # if the term name is default-* we will render this into the
    # appropriate default term name to be used in this filter.
    is_default_term = re.match(r"^ipv(4|6)\-default\-.*", self.term.name,
                               re.IGNORECASE)

    if (not has_match_criteria and not is_default_term):
      # this term doesn't match on anything and isn't a default-term
      logging.warning(
          "WARNING: term %s has no valid match criteria and "
          "will not be rendered.",
          self.term.name,
      )
      return ""

    else:
      # source address
      src_addr = self.term.GetAddressOfVersion("source_address", term_af)
      src_addr_ex = self.term.GetAddressOfVersion("source_address_exclude",
                                                  term_af)

      if src_addr:
        src_str = "source prefix"
        if src_addr_ex:
          # this should correspond to the generated field set
          src_str += " field-set src-%s" % self.term.name
        else:
          for addr in src_addr:
            src_str += " %s" % addr

        term_block.append([MATCH_INDENT, src_str, False])
      elif self.term.source_address:
        logging.debug(
            self.NO_AF_LOG_ADDR.substitute(
                term=self.term.name, direction="source", af=self.term_type))
        return ""

      # destination address
      dst_addr = self.term.GetAddressOfVersion("destination_address", term_af)
      dst_addr_ex = self.term.GetAddressOfVersion("destination_address_exclude",
                                                  term_af)

      if dst_addr:
        dst_str = "destination prefix"
        if dst_addr_ex:
          # this should correspond to the generated field set
          dst_str += " field-set dst-%s" % self.term.name
        else:
          for addr in dst_addr:
            dst_str += " %s" % addr

        term_block.append([MATCH_INDENT, dst_str, False])

      elif self.term.destination_address:
        logging.debug(
            self.NO_AF_LOG_ADDR.substitute(
                term=self.term.name, direction="destination",
                af=self.term_type))
        return ""

      if self.term.source_prefix:
        src_pfx_str = "source prefix field-set"
        for pfx in self.term.source_prefix:
          src_pfx_str += " %s" % pfx

        term_block.append([MATCH_INDENT, " %s" % src_pfx_str, False])

      if self.term.destination_prefix:
        dst_pfx_str = "destination prefix field-set"
        for pfx in self.term.destination_prefix:
          dst_pfx_str += " %s" % pfx

        term_block.append([MATCH_INDENT, " %s" % dst_pfx_str, False])

      # PROTOCOL MATCHES
      protocol_str = ""
      if self.term.protocol:
        protocol_str = self._processProtocol(self.term_type, self.term, flags)

      # protocol-except handling
      if self.term.protocol_except:
        protocol_str = self._processProtocolExcept(self.term_type, self.term,
                                                   flags)

      # tcp/udp port generation
      port_str = self._processPorts(self.term)
      if port_str:
        protocol_str += port_str

      # icmp[v6] handling
      icmp_type_str = ""
      icmp_code_str = ""
      if self.term.protocol == ["icmp"] or \
         self.term.protocol == ["icmpv6"]:
        icmp_type_str, icmp_code_str = self._processICMP(self.term)

      if self.term.icmp_type:
        protocol_str += icmp_type_str
      if self.term.icmp_code:
        protocol_str += icmp_code_str

      # don't render empty protocol strings.
      if protocol_str:
        term_block.append([MATCH_INDENT, protocol_str, False])

      # ADDITIONAL SUPPORTED MATCH OPTIONS ------------------------------
      # packet length
      if self.term.packet_length:
        term_block.append(
            [MATCH_INDENT,
             "ip length %s" % self.term.packet_length, False])

      # fragment offset
      if self.term.fragment_offset:
        term_block.append([
            MATCH_INDENT,
            "fragment offset %s" % self.term.fragment_offset, False
        ])

      if self.term.hop_limit:
        term_block.append([MATCH_INDENT, "ttl %s" % self.term.hop_limit, False])

      if self.term.ttl:
        term_block.append([MATCH_INDENT, "ttl %s" % self.term.ttl, False])

      if misc_options:
        for mopt in misc_options:
          term_block.append([MATCH_INDENT, mopt, False])

    # ACTION HANDLING
    # if there's no action, then this is an implicit permit
    current_action = self._ACTIONS.get(self.term.action[0])
    # non-permit/drop actions should be added here
    has_extra_actions = (
        self.term.logging or self.term.counter or self.term.dscp_set)

    # if !accept - generate an action statement
    # if accept and there are extra actions generate an actions statement
    # if accept and no extra actions don't generate an actions statement
    if self.term.action != ["accept"]:
      term_block.append([MATCH_INDENT, "actions", False])
      term_block.append([ACTION_INDENT, "%s" % current_action, False])
    elif self.term.action == ["accept"] and has_extra_actions:
      term_block.append([MATCH_INDENT, "actions", False])

    if has_extra_actions:
      # logging - only supported on deny actions
      if self.term.logging and self.term.action != ["accept"]:
        term_block.append([ACTION_INDENT, "log", False])
      elif self.term.logging and self.term.action == ["accept"]:
        logging.warning(
            "WARNING: term %s uses logging option but is not a deny "
            "action. logging will not be added.",
            self.term.name,
        )

        # counters
      if self.term.counter:
        term_block.append(
            [ACTION_INDENT,
             "count %s" % self.term.counter, False])

      term_block.append([MATCH_INDENT, "!", False])  # end of actions
    term_block.append([TERM_INDENT, "!", False])  # end of match entry

    for tindent, tstr, tverb in term_block:
      config.Append(tindent, tstr, verbatim=tverb)

    return str(config)

  def _reflowComments(self, comments, max_length):
    """reflows capirca comments to stay within max_length.

    Args:
      comments (list): list of comment strings
      max_length (int):

    Returns:
      type: list containing the reflowed text.

    if a comment list entry is > max_length it will be reflowed and appended
    to the returned comment list

    """
    flowed_comments = []

    for comment in comments:
      lines = comment.split("\n")
      for line in lines:
        if len(line) > max_length:
          line = textwrap.wrap(line, max_length)
          flowed_comments.extend(line)
        else:
          flowed_comments.append(line)

    return flowed_comments

  def _processPorts(self, term):
    port_str = ""

    # source port generation
    if term.source_port:
      port_str += " source port %s" % self._Group(term.source_port)

    # destination port
    if term.destination_port:
      port_str += (" destination port %s" % self._Group(term.destination_port))

    return port_str

  def _processICMP(self, term):
    icmp_types = [""]
    icmp_code_str = ""
    icmp_type_str = " type "

    if term.icmp_type:
      icmp_types = self.NormalizeIcmpTypes(term.icmp_type, term.protocol,
                                           self.term_type)
    if icmp_types != [""]:
      for t in icmp_types:
        icmp_type_str += "%s," % t

      if icmp_type_str.endswith(","):
        icmp_type_str = icmp_type_str[:-1]  # chomp trailing ','
        if not term.icmp_code:
          icmp_type_str += " code all"

    if self.term.icmp_code and len(icmp_types) <= 1:
      icmp_codes = self._Group(self.term.icmp_code)
      icmp_codes = re.sub(r" ", ",", icmp_codes)
      icmp_code_str += " code %s" % icmp_codes

    return icmp_type_str, icmp_code_str

  def _processProtocol(self, term_type, term, flags):
    anet_proto_map = {
        "inet": {
            # <1-255> protocol  values(s) or range(s) of protocol  values
            "ahp": "",
            "bgp": "",
            "icmp": "",
            "igmp": "",
            "ospf": "",
            "pim": "",
            "rsvp": "",
            "tcp": "",
            "udp": "",
            "vrrp": "",
        },
        "inet6": {
            # <0-255> protocol  values(s) or range(s) of protocol  values
            "bgp": "",  # BGP
            "icmpv6": "",  # ICMPv6 (58)
            "ospf": "",  # OSPF routing protocol (89)
            "pim": "",  # Protocol Independent Multicast (PIM) (103)
            "rsvp": "",  # Resource Reservation Protocol (RSVP) (46)
            "tcp": "",  # TCP
            "udp": "",  # UDP
            "vrrp": "",  # VRRP (112)
        }
    }

    protocol_str = ""
    prots = []
    # if there are dirty prots we'll need to convert the protocol list to
    # all numbers and generate the list of protocols to match on. EOS
    # doesn't support commingling named protocols w/numeric protocol-ids
    dirty_prots = False
    for p in term.protocol:
      if p not in anet_proto_map[term_type].keys():
        dirty_prots = True

      prots.append(p)

    if dirty_prots:
      num_prots = []
      for p in prots:
        try:
          num_prots.append(str(self.PROTO_MAP[p]))
        except KeyError:
          num_prots.append(str(p))
      protocol_str += "protocol %s" % ",".join(num_prots)
    else:
      protocol_str += "protocol %s" % self._Group(prots)

    if prots == ["tcp"] and flags:
      protocol_str += " flags " + " ".join(flags)

    return protocol_str

  def _processProtocolExcept(self, term_type, term, flags):
    # EOS does not have a protocol-except keyword. it does, however, support
    # lists of protocol-ids. given a term this function will generate the
    # appropriate list of protocol-id's which *will* be permited. within the
    # supported range of addaress family protocols.
    protocol_range = {
        "inet": 1,
        "inet6": 0,
    }
    protocol_str = ""
    except_list = set()
    for p in term.protocol_except:
      if p in self.PROTO_MAP.keys():
        except_list.add(self.PROTO_MAP[p])
      else:
        except_list.add(int(p))
    except_list = sorted(except_list)

    ex_str = ""
    ptr = protocol_range[term_type]
    for p in except_list:
      if 255 > p > ptr:
        if (p - 1) == ptr:
          ex_str += str(ptr) + ","
        else:
          ex_str += str(ptr) + "-" + str(p - 1) + ","

        ptr = p + 1
      elif p == ptr:
        ptr = p + 1

    ex_str += str(ptr) + "-" + "255"
    protocol_str = "protocol " + ex_str

    return protocol_str

  def _processTermOptions(self, term, options):
    flags = []
    misc_options = []

    for opt in [str(x) for x in options]:
      # note: traffic policies support additional tcp flags. for now,
      # only handle the required elements
      #
      # only append tcp-established for option established when
      # tcp is the only protocol
      if opt.startswith("established"):
        if self.term.protocol == ["tcp"] and "established" not in flags:
          flags.append("established")
      # if tcp-established specified, but more than just tcp is
      # included in the protocols, raise an error
      elif opt.startswith("tcp-established"):
        if (self.term.protocol == ["tcp"] and "established" not in flags):
          flags.append("established")
        if (len(self.term.protocol) > 1 or self.term.protocol != ["tcp"]):
          raise TcpEstablishedWithNonTcpError(
              "tcp-established can only be used with tcp "
              "protocol in term %s" % self.term.name)
      elif (opt.startswith("initial") and self.term.protocol == ["tcp"]):
        flags.append("initial")
      elif opt.startswith("rst") and self.term.protocol == ["tcp"]:
        flags.append("rst")
      elif "fragment" in opt:
        # handles the is-fragment and first-fragment options
        misc_options.append("fragment")

    return flags, misc_options

  def _Group(self, group, lc=True):
    """If 1 item return it, else return [item1 item2].

    Args:
      group: a list.  could be a list of strings(protocols) or a list of
             tuples(ports)
      lc: return a lower cased result for text.  Default is True.

    Returns:
      string: surrounded by '[' and '];' if len(group) > 1, or with
              just ';' appended if len(group) == 1
    """

    def _FormattedGroup(el, lc=True):
      """Return the actual formatting of an individual element.

      Args:
        el: either a string(protocol) or a tuple(ports)
        lc: return lower cased result for text.  Default is True.

      Returns:
        string: either the lower()'ed string or the ports, hyphenated
                if they're a range, or by itself if it's not.
      """
      if isinstance(el, str):
        if lc:
          return el
        else:
          return el.lower()
      elif isinstance(el, int):
        return str(el)
      # type is a tuple below here
      elif el[0] == el[1]:
        return "%d" % el[0]
      else:
        return "%d-%d" % (el[0], el[1])

    if len(group) > 1:
      rval = " ".join([_FormattedGroup(x, lc) for x in group])
    else:
      rval = _FormattedGroup(group[0])
    return rval


class AristaTrafficPolicy(aclgenerator.ACLGenerator):
  """arista traffic-policy rendering class.

  takes a policy object and renders the output into a syntax
  which is understood by arista switches.

  Attributes:
    pol: policy.Policy object
  """
  _AF_MAP = {"inet": 4, "inet6": 6}
  _DEFAULT_PROTOCOL = "ip"
  _PLATFORM = "arista_tp"
  _SUPPORTED_AF = frozenset(("inet", "inet6", "mixed"))
  _TERM = Term
  _LOGGING = set()

  SUFFIX = ".atp"

  def _BuildTokens(self):
    """returns: tuple of supported tokens and sub tokens."""
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {
        "action", "comment", "counter", "destination_address",
        "destination_address_exclude", "destination_port", "destination_prefix",
        "dscp_set", "expiration", "fragment_offset", "hop_limit", "icmp_code",
        "icmp_type", "logging", "name", "option", "owner", "packet_length",
        "platform", "platform_exclude", "port", "protocol", "protocol_except",
        "source_address", "source_address_exclude", "source_port",
        "source_prefix", "ttl", "verbatim"
    }
    supported_sub_tokens.update({
        "option": {
            "established",
            "is-fragment",
            ".*",  # accept arbitrary options
            "tcp-established",
            "tcp-initial",
        }
    })
    return supported_tokens, supported_sub_tokens

  def _MinimizePrefixes(self, include, exclude):
    """Calculate a minimal set of prefixes for match conditions.

    Args:
      include: Iterable of nacaddr objects, prefixes to match.
      exclude: Iterable of nacaddr objects, prefixes to exclude.

    Returns:
      A tuple (I,E) where I and E are lists containing the minimized
      versions of include and exclude, respectively.  The order
      of each input list is preserved.
    """
    # Remove any included prefixes that have EXACT matches in the
    # excluded list.  Excluded prefixes take precedence on the router
    # regardless of the order in which the include/exclude are applied.
    exclude_set = set(exclude)
    include_result = [ip for ip in include if ip not in exclude_set]

    # Every address match condition on a AristaTp firewall filter
    # contains an implicit "0/0 except" or "0::0/0 except".  If an
    # excluded prefix is not contained within any less-specific prefix
    # in the included set, we can elide it.  In other words, if the
    # next-less-specific prefix is the implicit "default except",
    # there is no need to configure the more specific "except".
    exclude_result = []
    for exclude_prefix in exclude:
      for include_prefix in include_result:
        if exclude_prefix.subnet_of(include_prefix):
          exclude_result.append(exclude_prefix)
          break

    return include_result, exclude_result

  def _GenPrefixFieldset(self, direction, name, pfxs, ex_pfxs, af):
    field_list = ""

    for p in pfxs:
      field_list += (" " * 6) + "%s\n" % p
    for p in ex_pfxs:
      field_list += (" " * 6) + "except %s\n" % p

    fieldset_hdr = ("field-set " + af + " prefix " + direction + "-" +
                    ("%s" % name) + "\n")
    field_set = fieldset_hdr + field_list
    return field_set

  def _TranslatePolicy(self, pol, exp_info):
    self.arista_traffic_policies = []
    af_map_txt = {"inet": "ipv4", "inet6": "ipv6"}

    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)
      filter_name = header.FilterName(self._PLATFORM)
      noverbose = "noverbose" in filter_options[1:]

      term_names = set()
      new_terms = []  # list of generated terms
      policy_field_sets = []  # list of generated field-sets
      policy_counters = set()  # set of the counters in the policy

      # default to mixed policies
      filter_type = "mixed"
      if len(filter_options) > 1:
        filter_type = filter_options[1]

      # if the filter_type is mixed, we need to iterate through the
      # supported address families. treat the incoming policy term
      # (pol_term) as a template for the term and override the necessary
      # elements of the term for the inet6 evaluation.
      #
      # only make a copy of the pol_term if filter_type = "mixed"
      ftypes = []

      if filter_type == "mixed":
        ftypes = ["inet", "inet6"]
      else:
        ftypes = [filter_type]

      for pol_term in terms:
        for ft in ftypes:
          if filter_type == "mixed":
            term = copy.deepcopy(pol_term)
          else:
            term = pol_term

          # if the term name is default-* we will render this into the
          # appropriate default term name to be used in this filter.
          default_term = re.match(r"^default\-.*", term.name, re.IGNORECASE)

          # TODO(sulrich): if term names become unique to address
          # families, this can be removed.
          if (filter_type == "mixed" and ft == "inet6"):
            term.name = af_map_txt[ft] + "-" + term.name

          if default_term:
            term.name = af_map_txt[ft] + "-default-all"

          if term.name in term_names:
            raise aclgenerator.DuplicateTermError("multiple terms named: %s" %
                                                  term.name)
          term_names.add(term.name)

          term = self.FixHighPorts(term, af=ft)
          if not term:
            continue

          if term.expiration:
            if term.expiration <= exp_info_date:
              logging.info(
                  "INFO: term %s in policy %s expires "
                  "in less than two weeks.",
                  term.name,
                  filter_name,
              )
            if term.expiration <= current_date:
              logging.warning(
                  "WARNING: term %s in policy %s is expired and "
                  "will not be rendered.",
                  term.name,
                  filter_name,
              )
              continue

          # emit warnings for unsupported options / terms
          if term.option:
            unsupported_opts = []
            for opt in [str(x) for x in term.option]:
              if opt.startswith("sample") or \
                 opt.startswith("first-fragment"):
                unsupported_opts.append(opt)

            # unsupported options are in use and should be skipped
            if unsupported_opts:
              logging.warning(
                  "WARNING: term %s in policy %s uses an "
                  "unsupported option (%s) and will not be "
                  "rendered.",
                  term.name,
                  filter_name,
                  " ".join(unsupported_opts),
              )
              continue

          has_unsupported_match_criteria = (
              term.dscp_except or term.dscp_match or term.ether_type or
              term.flexible_match_range or term.forwarding_class or
              term.forwarding_class_except or term.next_ip or term.port or
              term.traffic_type)
          if has_unsupported_match_criteria:
            logging.warning(
                "WARNING: term %s in policy %s uses an "
                "unsupported match criteria and will not "
                "be rendered.",
                term.name,
                filter_name,
            )
            continue

          if (("is-fragment" in term.option or "fragment" in term.option) and
              filter_type == "inet6"):
            raise AristaTpFragmentInV6Error("the term %s uses is-fragment but "
                                            "is a v6 policy." % term.name)

          # this should error out more gracefully in mixed configs
          if (("is-fragment" in term.option or "fragment" in term.option) and
              ft == "inet6"):
            logging.warning(
                "WARNING: term %s in mixed policy %s uses "
                "fragment the ipv6 version of the term will not be "
                "rendered.",
                term.name,
                filter_name,
            )
            continue

          # check for traffic-policy specific feature interactions
          if (("is-fragment" in term.option or "fragment" in term.option) and
              (term.source_port or term.destination_port)):
            logging.warning(
                "WARNING: term %s uses fragment as well as src/dst "
                "port matches.  traffic-policies currently do not "
                "support this match combination. the term will not "
                "be rendered",
                term.name,
            )
            continue

          # check for common unsupported actions (e.g.: next)
          if term.action == ["next"]:
            logging.warning(
                "WARNING: term %s uses an unsupported action "
                "(%s) and will not be rendered",
                term.name,
                " ".join(term.action),
            )
            continue

          # generate the prefix sets when there are inline addres
          # exclusions in a term. these will be referenced within the
          # term
          if term.source_address_exclude:
            src_addr = term.GetAddressOfVersion("source_address",
                                                self._AF_MAP[ft])
            src_addr_ex = term.GetAddressOfVersion("source_address_exclude",
                                                   self._AF_MAP[ft])
            src_addr, src_addr_ex = self._MinimizePrefixes(
                src_addr, src_addr_ex)

            if src_addr_ex:
              fs = self._GenPrefixFieldset("src", "%s" % term.name, src_addr,
                                           src_addr_ex, af_map_txt[ft])
              policy_field_sets.append(fs)

          if term.destination_address_exclude:
            dst_addr = term.GetAddressOfVersion("destination_address",
                                                self._AF_MAP[ft])
            dst_addr_ex = term.GetAddressOfVersion(
                "destination_address_exclude", self._AF_MAP[ft])
            dst_addr, dst_addr_ex = self._MinimizePrefixes(
                dst_addr, dst_addr_ex)

            if dst_addr_ex:
              fs = self._GenPrefixFieldset("dst", "%s" % term.name,
                                           term.destination_address,
                                           term.destination_address_exclude,
                                           af_map_txt[ft])
              policy_field_sets.append(fs)

          # generate the unique list of named counters
          if term.counter:
            # we can't have '.' in counter names
            term.counter = re.sub(r"\.", "-", str(term.counter))
            policy_counters.add(term.counter)

          new_terms.append(self._TERM(term, ft, noverbose))

      self.arista_traffic_policies.append(
          (header, filter_name, filter_type, new_terms, policy_counters,
           policy_field_sets))

  def __str__(self):
    config = Config()

    for (
        _,
        filter_name,
        _,
        terms,
        counters,
        field_sets,
    ) in self.arista_traffic_policies:
      # add the header information
      config.Append("", "traffic-policies")

      if field_sets:
        for fs in field_sets:
          config.Append("   ", fs)
          config.Append("   ", "!")

      config.Append("   ", "no traffic-policy %s" % filter_name)
      config.Append("   ", "traffic-policy %s" % filter_name)

      # if there are counters, export the list of counters
      if counters:
        str_counters = " ".join(counters)
        config.Append("   ", "counter %s" % str_counters)

      for term in terms:
        term_str = str(term)
        if term_str:
          config.Append("", term_str, verbatim=True)

    return str(config) + "\n"


# capirca/lib/aruba.py

# Copyright 2017 Google 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.
#

"""Aruba generator."""

import datetime
import logging

from capirca.lib import aclgenerator

_PLATFORM = 'aruba'
_COMMENT_MARKER = '#'
_TERMINATOR_MARKER = '!'


class Error(Exception):
  """Base error class."""


class Term(aclgenerator.Term):
  """A single Aruba ACL term, mostly used for the __str__() method.

  Args:
    term: policy.Term object.
    filter_type: IP address version number.
  """

  _ANY_STR = 'any'
  _ALIAS_STR = 'alias'
  _IPV6_START_STR = 'ipv6'
  _NET_DEST_STR = 'netdestination'
  _NEGATOR = 'no'
  _SRC_NETDEST_SUF = '_src'
  _DST_NETDEST_SUF = '_dst'
  _NETWORK_STRING = 'network'
  _HOST_STRING = 'host'
  _USER_STR = 'user'
  _SOURCE_IS_USER_OPT_STR = 'source-is-user'
  _DESTINATION_IS_USER_OPT_STR = 'destination-is-user'
  _NEGATE_OPT_STR = 'negate'
  _IDENT = '  '

  _COMMENT_LINE_LENGTH = 70

  _ACTIONS = {
      'accept': 'permit',
      'deny': 'deny',
  }

  _PROTOCOL_MAP = {
      'icmp': 1,
      'gre': 47,
      'esp': 50,
  }

  def __init__(self, term, filter_type, verbose=True):
    super().__init__(term)
    self.term = term
    self.filter_type = filter_type
    self.netdestinations = []
    self.verbose = verbose

  def __str__(self):
    netdestinations = []
    ret_str = []
    term_af = self.AF_MAP.get(self.filter_type)

    if self.term.verbatim:
      for next_verbatim in self.term.verbatim:
        if next_verbatim[0] == _PLATFORM and next_verbatim[1]:
          ret_str.append('%s%s' % (self._IDENT, next_verbatim[1]))

      return '\n'.join(t for t in ret_str if t)
    if self.verbose:
      comments = self.term.comment[:]

      if self.term.owner:
        comments.append('Owner: %s' % self.term.owner)

      if comments:
        for line in aclgenerator.WrapWords(comments,
                                           self._COMMENT_LINE_LENGTH):
          ret_str.append('%s%s %s' % (self._IDENT, _COMMENT_MARKER, line))

    src_addr_token = ''
    dst_addr_token = ''

    if self._SOURCE_IS_USER_OPT_STR in self.term.option:
      src_addr_token = self._USER_STR
    else:
      if self.term.source_address:
        src_addr = self.term.GetAddressOfVersion('source_address',
                                                 term_af)
        if not src_addr:
          return ''

        src_netdest_id = '%s%s' % (self.term.name.lower(),
                                   self._SRC_NETDEST_SUF)
        src_addr_token = '%s %s' % (self._ALIAS_STR, src_netdest_id)
        netdestinations.append(self._GenerateNetdest(src_netdest_id,
                                                     src_addr,
                                                     term_af))

      else:
        src_addr_token = self._ANY_STR

    if self._DESTINATION_IS_USER_OPT_STR in self.term.option:
      dst_addr_token = self._USER_STR
    else:
      if self.term.destination_address:
        dst_addr = self.term.GetAddressOfVersion('destination_address', term_af)
        if not dst_addr:
          return ''

        dst_netdest_id = '%s%s' % (self.term.name.lower(),
                                   self._DST_NETDEST_SUF)
        dst_addr_token = '%s %s' % (self._ALIAS_STR, dst_netdest_id)
        netdestinations.append(self._GenerateNetdest(dst_netdest_id,
                                                     dst_addr,
                                                     term_af))
      else:
        dst_addr_token = self._ANY_STR

    dst_protocol_list = []
    if self.term.protocol:
      dst_protocol_list = self._GeneratePortTokens(self.term.protocol,
                                                   self.term.destination_port)
    else:
      dst_protocol_list = [self._ANY_STR]

    for dst_port in dst_protocol_list:
      str_tok = [' ']

      if self._NEGATE_OPT_STR in self.term.option:
        str_tok.append(self._NEGATOR)

      if term_af == 6:
        str_tok.append(self._IPV6_START_STR)

      str_tok.append(src_addr_token)
      str_tok.append(dst_addr_token)

      str_tok.append(dst_port)
      str_tok.append(self._ACTIONS.get(self.term.action[0]))
      ret_str.append(' '.join(t for t in str_tok if t))

    self.netdestinations = netdestinations

    return '\n'.join(t for t in ret_str if t)

  def _GenerateNetdest(self, addr_netdestid, addresses, af):
    """Generates the netdestinations text block.

    Args:
      addr_netdestid: netdestinations identifier.
      addresses: IP addresses.
      af: address family.
    Returns:
      A text block suitable for netdestinations in Aruba ACLs.
    """
    ret_str = []

    # Aruba does not use IP version identifier for IPv4.
    addr_family = '6' if af == 6 else ''

    ret_str.append('%s %s' % (self._NET_DEST_STR + addr_family,
                              addr_netdestid))

    for address in addresses:
      ret_str.append('%s%s' % (self._IDENT,
                               self._GenerateNetworkOrHostTokens(address)))

    ret_str.append('%s\n' % _TERMINATOR_MARKER)

    return '\n'.join(t for t in ret_str if t)

  def _GenerateNetworkOrHostTokens(self, address):
    """Generates the text block host or network identifier for netdestinations.

    Args:
      address: IP address.
    Returns:
      A string line using either 'host' or 'network', properly formatted for
      Aruba ACLs.
    """
    if address.num_addresses == 1:
      return '%s %s' % (self._HOST_STRING, address.network_address)

    if address.version == 6:
      return '%s %s/%s' % (self._NETWORK_STRING,
                           address.network_address,
                           address.prefixlen)

    return '%s %s %s' % (self._NETWORK_STRING,
                         address.network_address,
                         address.netmask)

  def _GeneratePortTokens(self, protocols, ports):
    """Generates string tokens for ports.

    Args:
      protocols: protocol to use (e.g. tcp, udp, etc.)
      ports: port number.
    Returns:
      A list of strings to be used as the port selector in Aruba ACLs.
    """
    ret_ports = []

    for protocol in protocols:
      if protocol in self._PROTOCOL_MAP:
        return [str(self._PROTOCOL_MAP[protocol])]

      for start_port, end_port in ports:
        ret_ports.append('%s %s' %
                         (protocol.lower(), ' '.join(
                             str(x) for x in set([start_port, end_port]))))

    return ret_ports


class Aruba(aclgenerator.ACLGenerator):
  """An Aruba policy object.

  This class takes a policy object and renders the output (via __str__ method)
  into a syntax which is understood by Aruba devices.

  Args:
    pol: policy.Policy object.
  """

  SUFFIX = '.aacl'

  _ACL_LINE_HEADER = 'ip access-list session'

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      Tuple containing both supported tokens and sub tokens.
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens -= {
        'destination_address_exclude',
        'icmp_type',
        'source_port',
        'source_address_exclude',
        'platform',
        'platform_exclude',
    }

    supported_sub_tokens.update({
        'action': {
            'accept',
            'deny',
        },
        'option': {
            'source-is-user',
            'destination-is-user',
            'negate',
        },
    })

    del supported_sub_tokens['icmp_type']

    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.aruba_policies = []

    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    for header, terms in pol.filters:
      filter_name = header.FilterName(_PLATFORM)
      filter_options = header.FilterOptions(_PLATFORM)
      verbose = True
      if 'noverbose' in filter_options:
        filter_options.remove('noverbose')
        verbose = False

      filter_type = 'inet'
      if 'inet6' in filter_options:
        filter_type += '6'

      new_terms = []
      for term in terms:
        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)

          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue

        new_terms.append(Term(term, filter_type, verbose))

      self.aruba_policies.append((filter_name, new_terms, filter_type))

  def __str__(self):
    target = []

    target.extend(aclgenerator.AddRepositoryTags('%s ' % _COMMENT_MARKER))

    for filter_name, terms, _ in self.aruba_policies:
      netdestinations = []
      term_strings = []

      for term in terms:
        term_strings.append(str(term))
        netdestinations.extend(term.netdestinations)

      target.extend(netdestinations)
      target.append('%s %s' % (self._ACL_LINE_HEADER, filter_name))
      target.extend(term_strings)
      target.extend(_TERMINATOR_MARKER)

    if target:
      target.append('')

    return '\n'.join(target)


# capirca/lib/brocade.py

# Copyright 2015 Google 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.
#

"""Brocade generator."""

from capirca.lib import cisco


class Brocade(cisco.Cisco):
  """A brocade policy object.

  Brocade devices do not like protocol numbers. Revert the protocol numbers to
  names just before emitting acl lines to minimize difference from Cisco logic.
  """

  _PLATFORM = 'brocade'
  SUFFIX = '.bacl'
  # Protocols should be emitted as they were in the policy (names).
  _PROTO_INT = False
  _TERM_REMARK = False


# capirca/lib/cisco.py

# Copyright 2011 Google 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.
#

"""Cisco generator."""

import datetime
import ipaddress
from typing import cast, Union

from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import summarizer


_ACTION_TABLE = {
    'accept': 'permit',
    'deny': 'deny',
    'reject': 'deny',
    'next': '! next',
    'reject-with-tcp-rst': 'deny',  # tcp rst not supported
}

_COMMENT_MAX_WIDTH = 70


# generic error class
class Error(Exception):
  """Generic error class."""


class CiscoDuplicateTermError(Error):
  """Raised on duplicate term names."""


class CiscoNextIpError(Error):
  """Raised when next-ip is misconfigured."""


class UnsupportedCiscoAccessListError(Error):
  """Raised when we're give a non named access list."""


class StandardAclTermError(Error):
  """Raised when there is a problem in a standard access list."""


class ExtendedACLTermError(Error):
  """Raised when there is a problem in an extended access list."""


class TermStandard:
  """A single standard ACL Term."""

  def __init__(self, term, filter_name, platform='cisco', verbose=True):
    self.term = term
    self.filter_name = filter_name
    self.platform = platform
    self.options = []
    self.logstring = ''
    self.dscpstring = ''
    self.verbose = verbose
    # sanity checking for standard acls
    if self.term.protocol:
      raise StandardAclTermError(
          'Standard ACLs cannot specify protocols')
    if self.term.icmp_type:
      raise StandardAclTermError(
          'ICMP Type specifications are not permissible in standard ACLs')
    if (self.term.source_address
        or self.term.source_address_exclude
        or self.term.destination_address
        or self.term.destination_address_exclude):
      raise StandardAclTermError(
          'Standard ACLs cannot use source or destination addresses')
    if self.term.option:
      raise StandardAclTermError(
          'Standard ACLs prohibit use of options')
    if self.term.source_port or self.term.destination_port:
      raise StandardAclTermError(
          'Standard ACLs prohibit use of port numbers')
    if self.term.logging:
      logging.warning(
          'WARNING: Standard ACL logging is set in filter %s, term %s and '
          'may not implemented on all IOS versions', self.filter_name,
          self.term.name)
      self.logstring = ' log'
    if self.term.dscp_match:
      logging.warning(
          'WARNING: dscp-match is set in filter %s, term %s and may not be '
          'implemented on all IOS version', self.filter_name, self.term.name)
      self.dscpstring = ' dscp' + self.term.dscp_match

  def __str__(self):
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if self.platform not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if self.platform in self.term.platform_exclude:
        return ''

    ret_str = []

    # Term verbatim output - this will skip over normal term creation
    # code by returning early.  Warnings provided in policy.py.
    if self.term.verbatim:
      for next_verbatim in self.term.verbatim:
        if next_verbatim[0] == self.platform:
          ret_str.append(str(next_verbatim[1]))
      return '\n'.join(ret_str)

    v4_addresses = [x for x in self.term.address if
                    not isinstance(x, nacaddr.IPv6)]
    if self.filter_name.isdigit():
      if self.verbose:
        ret_str.append('access-list %s remark %s' % (self.filter_name,
                                                     self.term.name))
        comments = aclgenerator.WrapWords(self.term.comment,
                                          _COMMENT_MAX_WIDTH)
        for comment in comments:
          ret_str.append('access-list %s remark %s' % (self.filter_name,
                                                       comment))

      action = _ACTION_TABLE.get(str(self.term.action[0]))
      if v4_addresses:
        for addr in v4_addresses:
          if addr.prefixlen == 32:
            ret_str.append('access-list %s %s %s%s%s' % (self.filter_name,
                                                         action,
                                                         addr.network_address,
                                                         self.logstring,
                                                         self.dscpstring))
          else:
            ret_str.append('access-list %s %s %s %s%s%s' % (
                self.filter_name,
                action,
                addr.network_address,
                addr.hostmask,
                self.logstring,
                self.dscpstring))
      else:
        ret_str.append('access-list %s %s %s%s%s' % (self.filter_name,
                                                     action,
                                                     'any',
                                                     self.logstring,
                                                     self.dscpstring))
    else:
      if self.verbose:
        ret_str.append(' remark ' + self.term.name)
        comments = aclgenerator.WrapWords(self.term.comment,
                                          _COMMENT_MAX_WIDTH)
        if comments and comments[0]:
          for comment in comments:
            ret_str.append(' remark ' + str(comment))

      action = _ACTION_TABLE.get(str(self.term.action[0]))
      if v4_addresses:
        for addr in v4_addresses:
          if addr.prefixlen == 32:
            ret_str.append(' %s host %s%s%s' % (action,
                                                addr.network_address,
                                                self.logstring,
                                                self.dscpstring))
          elif self.platform == 'arista':
            ret_str.append(' %s %s/%s%s%s' % (action,
                                              addr.network_address,
                                              addr.prefixlen,
                                              self.logstring,
                                              self.dscpstring))
          else:
            ret_str.append(' %s %s %s%s%s' % (action,
                                              addr.network_address,
                                              addr.hostmask,
                                              self.logstring,
                                              self.dscpstring))
      else:
        ret_str.append(' %s %s%s%s' % (action,
                                       'any',
                                       self.logstring,
                                       self.dscpstring))

    return '\n'.join(ret_str)


class ObjectGroup:
  """Used for printing out the object group definitions.

  since the ports don't store the token name information, we have
  to fudge their names.  ports will be written out like

    object-group port <low_port>-<high_port>
      range <low-port> <high-port>
    exit

  where as the addressess can be written as

    object-group address ipv4 first-term-source-address
      172.16.0.0
      172.20.0.0 255.255.0.0
      172.22.0.0 255.128.0.0
      172.24.0.0
      172.28.0.0
    exit
  """

  def __init__(self):
    self.filter_name = ''
    self.terms = []

  @property
  def valid(self):
    return bool(self.terms)

  def AddTerm(self, term):
    self.terms.append(term)

  def AddName(self, filter_name):
    self.filter_name = filter_name

  def __str__(self):
    ret_str = ['\n']
    # netgroups will contain two-tuples of group name string and family int.
    netgroups = set()
    ports = {}

    for term in self.terms:
      # I don't have an easy way get the token name used in the pol file
      # w/o reading the pol file twice (with some other library) or doing
      # some other ugly hackery. Instead, the entire block of source and dest
      # addresses for a given term is given a unique, computable name which
      # is not related to the NETWORK.net token name.  that's what you get
      # for using cisco, which has decided to implement its own meta language.

      # Create network object-groups
      addr_type = ('source_address', 'destination_address')
      addr_family = (4, 6)

      for source_or_dest in addr_type:
        for family in addr_family:
          addrs = term.GetAddressOfVersion(source_or_dest, family)
          if addrs:
            net_def_name = addrs[0].parent_token
            # We have addresses for this family and have not already seen it.
            if (net_def_name, family) not in netgroups:
              netgroups.add((net_def_name, family))
              ret_str.append('object-group network ipv%d %s' % (
                  family, net_def_name))
              for addr in addrs:
                ret_str.append(' %s/%s' % (addr.network_address,
                                           addr.prefixlen))
              ret_str.append('exit\n')

      # Create port object-groups
      for port in term.source_port + term.destination_port:
        if not port:
          continue
        port_key = '%s-%s' % (port[0], port[1])
        if port_key not in ports:
          ports[port_key] = True
          ret_str.append('object-group port %s' % port_key)
          if port[0] != port[1]:
            ret_str.append(' range %d %d' % (port[0], port[1]))
          else:
            ret_str.append(' eq %d' % port[0])
          ret_str.append('exit\n')

    return '\n'.join(ret_str)


class PortMap:
  """Map port numbers to service names."""
  # Define port mappings common to all protocols
  _PORTS_TCP = {
      179: 'bgp',
      19: 'chargen',
      514: 'cmd',
      13: 'daytime',
      9: 'discard',
      53: 'domain',
      7: 'echo',
      512: 'exec',
      79: 'finger',
      21: 'ftp',
      20: 'ftp-data',
      70: 'gopher',
      443: 'https',
      113: 'ident',
      194: 'irc',
      543: 'klogin',
      544: 'kshell',
      389: 'ldap',
      636: 'ldaps',
      513: 'login',
      515: 'lpd',
      2049: 'nfs',
      119: 'nntp',
      496: 'pim-auto-rp',
      109: 'pop2',
      110: 'pop3',
      1723: 'pptp',
      25: 'smtp',
      22: 'ssh',
      111: 'sunrpc',
      49: 'tacacs',
      517: 'talk',
      23: 'telnet',
      540: 'uucp',
      43: 'whois',
      80: 'www',
  }
  _PORTS_UDP = {
      512: 'biff',
      68: 'bootpc',
      67: 'bootps',
      9: 'discard',
      195: 'dnsix',
      53: 'domain',
      7: 'echo',
      500: 'isakmp',
      434: 'mobile-ip',
      42: 'nameserver',
      138: 'netbios-dgm',
      137: 'netbios-ns',
      2049: 'nfs',
      123: 'ntp',
      496: 'pim-auto-rp',
      520: 'rip',
      161: 'snmp',
      162: 'snmptrap',
      111: 'sunrpc',
      514: 'syslog',
      49: 'tacacs',
      517: 'talk',
      69: 'tftp',
      37: 'time',
      513: 'who',
      177: 'xdmcp',
  }
  _TYPES_ICMP = {
      6: 'alternate-address',
      31: 'conversion-error',
      8: 'echo',
      0: 'echo-reply',
      16: 'information-reply',
      15: 'information-request',
      18: 'mask-reply',
      17: 'mask-request',
      32: 'mobile-redirect',
      12: 'parameter-problem',
      5: 'redirect',
      9: 'router-advertisement',
      10: 'router-solicitation',
      4: 'source-quench',
      11: 'time-exceeded',
      14: 'timestamp-reply',
      13: 'timestamp-request',
      30: 'traceroute',
      3: 'unreachable',
  }

  # Combine cisco-specific port mappings with common ones
  _CISCO_PORTS_TCP = {
      5190: 'aol',
      1494: 'citrix-ica',
      2748: 'ctiqbe',
      1720: 'h323',
      101: 'hostname',
      143: 'imap4',
      750: 'kerberos',
      1352: 'lotusnotes',
      139: 'netbios-ssn',
      5631: 'pcanywhere-data',
      1521: 'sqlnet',
  }
  _CISCO_PORTS_TCP.update(_PORTS_TCP)
  _CISCO_PORTS_UDP = {
      750: 'kerberos',
      5632: 'pcanywhere-status',
      1645: 'radius',
      1646: 'radius-acct',
      5510: 'secureid-udp',
  }
  _CISCO_PORTS_UDP.update(_PORTS_UDP)

  # Combine arista-specific port mappings with common ones
  _ARISTA_PORTS_TCP = {
      143: 'imap',
      88: 'kerberos',
  }
  _ARISTA_PORTS_TCP.update(_PORTS_TCP)
  _ARISTA_PORTS_UDP = {
      88: 'kerberos',
      1812: 'radius',
      1813: 'radius-acct',
  }
  _ARISTA_PORTS_UDP.update(_PORTS_UDP)

  # Full port map data structure
  _PORT_MAP = {
      'cisco': {
          'tcp': _CISCO_PORTS_TCP,
          'udp': _CISCO_PORTS_UDP,
          'icmp': _TYPES_ICMP
      },
      'arista': {
          'tcp': _ARISTA_PORTS_TCP,
          'udp': _ARISTA_PORTS_UDP,
          'icmp': _TYPES_ICMP
      }
  }

  @staticmethod
  def GetProtocol(port_num, proto, platform='cisco'):
    """Converts a port number to a name or returns the number.

    Args:
      port_num: integer representing the port number.
      proto: string representing proto (tcp, udp, etc).
      platform: string representing platform (cisco, arista)

    Returns:
      A name of the protocol or the port number that was provided.
    """
    try:
      port_map = PortMap._PORT_MAP[platform][proto]
      return port_map[port_num]
    except KeyError:
      return port_num

class Term(aclgenerator.Term):
  """A single ACL Term."""
  ALLOWED_PROTO_STRINGS = ['eigrp', 'gre', 'icmp', 'igmp', 'igrp', 'ip',
                           'ipinip', 'nos', 'pim', 'tcp', 'udp',
                           'sctp', 'ahp']

  IPV4_ADDRESS = Union[nacaddr.IPv4, ipaddress.IPv4Network]
  IPV6_ADDRESS = Union[nacaddr.IPv6, ipaddress.IPv6Network]

  def __init__(self, term, af=4, proto_int=True, enable_dsmo=False,
               term_remark=True, platform='cisco', verbose=True):
    super().__init__(term)
    self.term = term
    self.proto_int = proto_int
    self.options = []
    self.enable_dsmo = enable_dsmo
    self.term_remark = term_remark
    self.platform = platform
    self.verbose = verbose
    # Our caller should have already verified the address family.
    assert af in (4, 6)
    self.af = af
    if af == 4:
      self.text_af = 'inet'
    else:
      self.text_af = 'inet6'

  def __str__(self):
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if self.platform not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if self.platform in self.term.platform_exclude:
        return ''

    ret_str = ['\n']

    # Don't render icmpv6 protocol terms under inet, or icmp under inet6
    if ((self.af == 6 and 'icmp' in self.term.protocol) or
        (self.af == 4 and 'icmpv6' in self.term.protocol)):
      logging.debug(self.NO_AF_LOG_PROTO.substitute(
          term=self.term.name,
          proto=', '.join(self.term.protocol),
          af=self.text_af))
      return ''
    if self.verbose:
      if self.term_remark:
        ret_str.append(' remark ' + self.term.name)
      if self.term.owner:
        self.term.comment.append('Owner: %s' % self.term.owner)
      for comment in self.term.comment:
        for line in comment.split('\n'):
          ret_str.append(' remark ' + str(line)[:100].rstrip())

    # Term verbatim output - this will skip over normal term creation
    # code by returning early.  Warnings provided in policy.py.
    if self.term.verbatim:
      for next_verbatim in self.term.verbatim:
        if next_verbatim[0] == self.platform:
          ret_str.append(str(next_verbatim[1]))
      return '\n'.join(ret_str)

    # protocol
    if not self.term.protocol:
      if self.af == 6:
        protocol = ['ipv6']
      elif self.platform == 'ciscoxr':
        protocol = ['ipv4']
      else:
        protocol = ['ip']
    elif self.term.protocol == ['hopopt']:
      protocol = ['hbh']
    elif self.proto_int:
      protocol = [proto if proto in self.ALLOWED_PROTO_STRINGS
                  else self.PROTO_MAP.get(proto)
                  for proto in self.term.protocol]
    else:
      protocol = self.term.protocol
    # Arista can not process acls with esp/ah, these must appear as integers.
    if self.platform == 'arista':
      if 'esp' in protocol:
        protocol = [x if x != 'esp' else '50' for x in protocol]
      if 'ah' in protocol:
        protocol = [x if x != 'ah' else '51' for x in protocol]

    # source address
    if self.term.source_address:
      source_address = self.term.GetAddressOfVersion('source_address', self.af)
      source_address_exclude = self.term.GetAddressOfVersion(
          'source_address_exclude', self.af)
      if source_address_exclude:
        source_address = nacaddr.ExcludeAddrs(
            source_address,
            source_address_exclude)
      if not source_address:
        logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                     direction='source',
                                                     af=self.text_af))
        return ''
      if self.enable_dsmo:
        source_address = summarizer.Summarize(source_address)
    else:
      # source address not set
      source_address = ['any']

    # destination address
    if self.term.destination_address:
      destination_address = self.term.GetAddressOfVersion(
          'destination_address', self.af)
      destination_address_exclude = self.term.GetAddressOfVersion(
          'destination_address_exclude', self.af)
      if destination_address_exclude:
        destination_address = nacaddr.ExcludeAddrs(
            destination_address,
            destination_address_exclude)
      if not destination_address:
        logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                     direction='destination',
                                                     af=self.text_af))
        return ''
      if self.enable_dsmo:
        destination_address = summarizer.Summarize(destination_address)
    else:
      # destination address not set
      destination_address = ['any']

    # options
    opts = [str(x) for x in self.term.option]
    if ((self.PROTO_MAP['tcp'] in protocol or 'tcp' in protocol)
        and ('tcp-established' in opts or 'established' in opts)):
      if 'established' not in self.options:
        self.options.append('established')
    # Using both 'fragments' and 'is-fragment', ref Github Issue #187
    if ('ip' in protocol) and (('fragments' in opts) or
      ('is-fragment' in opts)):
      if 'fragments' not in self.options:
        self.options.append('fragments')
    # ACL-based Forwarding
    if (self.platform == 'ciscoxr'
       ) and not self.term.action and self.term.next_ip and (
           'nexthop1' not in opts):
      if len(self.term.next_ip) > 1:
        raise CiscoNextIpError('The following term has more than one next IP '
                               'value: %s' % self.term.name)
      if (not isinstance(self.term.next_ip[0], nacaddr.IPv4) and
          not isinstance(self.term.next_ip[0], nacaddr.IPv6)):
        raise CiscoNextIpError('Next IP value must be an IP address. '
                               'Invalid term: %s' % self.term.name)
      if self.term.next_ip[0].num_addresses > 1:
        raise CiscoNextIpError('The following term has a subnet instead of a '
                               'host: %s' % self.term.name)
      nexthop = self.term.next_ip[0].network_address
      nexthop_protocol = 'ipv4' if nexthop.version == 4 else 'ipv6'
      self.options.append('nexthop1 %s %s' % (nexthop_protocol, nexthop))
      action = _ACTION_TABLE.get('accept')

    if self.term.action:
      action = _ACTION_TABLE.get(str(self.term.action[0]))

    # ports
    source_port = [()]
    destination_port = [()]
    if self.term.source_port:
      source_port = self._FixConsecutivePorts(self.term.source_port)

    if self.term.destination_port:
      destination_port = self._FixConsecutivePorts(self.term.destination_port)

    # logging
    if self.term.logging:
      self.options.append('log')

    # dscp; unlike srx, cisco only supports single, non-except values
    if self.term.dscp_match:
      if len(self.term.dscp_match) > 1:
        raise ExtendedACLTermError(
            'Extended ACLs cannot specify more than one dscp match value')
      else:
        self.options.append('dscp %s' % ' '.join(self.term.dscp_match))

    # icmp-types
    icmp_types = ['']
    if self.term.icmp_type:
      icmp_types = self.NormalizeIcmpTypes(self.term.icmp_type,
                                           self.term.protocol, self.af)
    icmp_codes = ['']
    if self.term.icmp_code:
      icmp_codes = self.term.icmp_code
    fixed_src_addresses = [self._GetIpString(x) for x in source_address]
    fixed_dst_addresses = [self._GetIpString(x) for x in destination_address]
    fixed_opts = {}
    for p in protocol:
      fixed_opts[p] = self._FixOptions(p, self.options)
    for saddr in fixed_src_addresses:
      for daddr in fixed_dst_addresses:
        for sport in source_port:
          for dport in destination_port:
            for proto in protocol:
              opts = fixed_opts[proto]
              # cisconx uses icmp for both ipv4 and ipv6
              if self.platform == 'cisconx':
                if self.af == 6:
                  proto = 'icmp' if proto == 'icmpv6' else proto
              for icmp_type in icmp_types:
                for icmp_code in icmp_codes:
                  ret_str.extend(
                      self._TermletToStr(action, proto, saddr,
                                         self._FormatPort(sport, proto), daddr,
                                         self._FormatPort(dport, proto),
                                         icmp_type, icmp_code, opts))

    return '\n'.join(ret_str)

  def _GetIpString(self, addr):
    """Formats the address object for printing in the ACL.

    Args:
      addr: str or ipaddr, address
    Returns:
      An address string suitable for the ACL.
    """
    if isinstance(addr, nacaddr.IPv4) or isinstance(addr,
                                                    ipaddress.IPv4Network):
      addr = cast(self.IPV4_ADDRESS, addr)
      if addr.num_addresses > 1:
        if self.platform == 'arista':
          return addr.with_prefixlen
        return '%s %s' % (addr.network_address, addr.hostmask)
      return 'host %s' % (addr.network_address)
    if isinstance(addr, nacaddr.IPv6) or isinstance(addr,
                                                    ipaddress.IPv6Network):
      addr = cast(self.IPV6_ADDRESS, addr)
      if addr.num_addresses > 1:
        return addr.with_prefixlen
      return 'host %s' % (addr.network_address)
    # DSMO enabled
    if isinstance(addr, summarizer.DSMNet):
      return '%s %s' % summarizer.ToDottedQuad(addr, negate=True)
    return addr

  def _FormatPort(self, port, proto):
    """Returns a formatted port string for the range.

    Args:
      port: str list or none, the port range.
      proto: str representing proto (tcp, udp, etc).

    Returns:
      A string suitable for the ACL.
    """
    if not port:
      return ''
    port0 = port[0]
    port1 = port[1]
    if self.platform == 'arista':
      port0 = PortMap.GetProtocol(port0, proto, self.platform)
      port1 = PortMap.GetProtocol(port1, proto, self.platform)

    if port[0] != port[1]:
      return 'range %s %s' % (port0, port1)
    return 'eq %s' % (port0)

  def _FixOptions(self, proto, option):
    """Returns a set of options suitable for the given protocol.

    Fix done:
    - Filter out 'established' for UDP.
    - Filter out 'fragments' for TCP/UDP

    Args:
      proto: str or int, protocol
      option: list or none, optional, eg. 'logging' tokens.
    Returns:
      A list of options suitable for that protocol.
    """
    # Prevent UDP from appending 'established' to ACL line
    sane_options = list(option)
    if ((proto == self.PROTO_MAP['udp'] or proto == 'udp')
        and 'established' in sane_options):
      sane_options.remove('established')
    return sane_options

  def _TermletToStr(self, action, proto, saddr, sport, daddr, dport,
                    icmp_type, icmp_code, option):
    """Take the various compenents and turn them into a cisco acl line.

    Args:
      action: str, action
      proto: str or int, protocol
      saddr: str, source address
      sport: str, the source port
      daddr: str, the destination address
      dport: str, the destination port
      icmp_type: icmp-type numeric specification (if any)
      icmp_code: icmp-code numeric specification (if any)
      option: list or none, optional, eg. 'logging' tokens.

    Returns:
      string of the cisco acl line, suitable for printing.

    Raises:
      UnsupportedCiscoAccessListError: When unknown icmp-types specified
    """
    # str(icmp_type) is needed to ensure 0 maps to '0' instead of FALSE
    icmp_type = str(icmp_type)
    icmp_code = str(icmp_code)
    all_elements = [action, str(proto), saddr, sport, daddr, dport, icmp_type,
                    icmp_code, ' '.join(option)]
    non_empty_elements = [x for x in all_elements if x]
    return [' ' + ' '.join(non_empty_elements)]

  def _FixConsecutivePorts(self, port_list):
    """Takes a list of tuples and expands the tuple if the range is two.

        http://www.cisco.com/warp/public/cc/pd/si/casi/ca6000/tech/65acl_wp.pdf

    Args:
      port_list: A list of tuples representing ports.

    Returns:
      list of tuples
    """
    temporary_port_list = []
    for low_port, high_port in port_list:
      if low_port == high_port - 1:
        temporary_port_list.append((low_port, low_port))
        temporary_port_list.append((high_port, high_port))
      else:
        temporary_port_list.append((low_port, high_port))
    return temporary_port_list


class ObjectGroupTerm(Term):
  """An individual term of an object-group'd acl.

  Object Group acls are very similar to extended acls in their
  syntax except they use a meta language with address/service
  definitions.

  eg:

    permit tcp first-term-source-address 179-179 ANY

  where first-term-source-address, ANY and 179-179 are defined elsewhere
  in the acl.
  """
  # Protocols should be emitted as integers rather than strings.
  _PROTO_INT = True

  def __init__(self, term, filter_name, platform='cisco', verbose=True):
    super().__init__(term)
    self.term = term
    self.filter_name = filter_name
    self.platform = platform
    self.verbose = verbose

  def __str__(self):
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if self.platform not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if self.platform in self.term.platform_exclude:
        return ''

    source_address_set = set()
    destination_address_set = set()
    ret_str = ['\n']
    if self.verbose:
      ret_str.append(' remark %s' % self.term.name)
      comments = aclgenerator.WrapWords(self.term.comment,
                                        _COMMENT_MAX_WIDTH)
      if comments and comments[0]:
        for comment in comments:
          ret_str.append(' remark %s' % str(comment))

    # Term verbatim output - this will skip over normal term creation
    # code by returning early.  Warnings provided in policy.py.
    if self.term.verbatim:
      for next_verbatim in self.term.verbatim:
        if next_verbatim[0] == self.platform:
          ret_str.append(str(next_verbatim[1]))
      return '\n'.join(ret_str)

    # protocol
    if not self.term.protocol:
      protocol = ['ip']

    else:
      protocol = [proto if proto in self.ALLOWED_PROTO_STRINGS
                  else self.PROTO_MAP.get(proto)
                  for proto in self.term.protocol]

    # addresses
    source_address = self.term.source_address
    if not self.term.source_address:
      source_address = [nacaddr.IPv4('0.0.0.0/0', token='any')]
    source_address_set.add(source_address[0].parent_token)

    destination_address = self.term.destination_address
    if not self.term.destination_address:
      destination_address = [nacaddr.IPv4('0.0.0.0/0', token='any')]
    destination_address_set.add(destination_address[0].parent_token)
    # ports
    source_port = [()]
    destination_port = [()]
    if self.term.source_port:
      source_port = self.term.source_port
    if self.term.destination_port:
      destination_port = self.term.destination_port
    for saddr in source_address_set:
      for daddr in destination_address_set:
        for sport in source_port:
          for dport in destination_port:
            for proto in protocol:
              ret_str.append(
                  self._TermletToStr(_ACTION_TABLE.get(str(
                      self.term.action[0])), proto, saddr, sport, daddr, dport))

    return '\n'.join(ret_str)

  def _TermletToStr(self, action, proto, saddr, sport, daddr, dport):
    """Output a portion of a cisco term/filter only, based on the 5-tuple."""
    # Empty addr/port destinations should emit 'any'
    if saddr and saddr != 'any':
      saddr = 'net-group %s' % saddr
    if daddr and daddr != 'any':
      daddr = 'net-group %s' % daddr
    # fix ports
    if sport:
      sport = ' port-group %d-%d' % (sport[0], sport[1])
    else:
      sport = ''
    if dport:
      dport = ' port-group %d-%d' % (dport[0], dport[1])
    else:
      dport = ''

    return (' %s %s %s%s %s%s' % (
        action, proto, saddr, sport, daddr, dport)).rstrip()


class Cisco(aclgenerator.ACLGenerator):
  """A cisco policy object."""

  _PLATFORM = 'cisco'
  _DEFAULT_PROTOCOL = 'ip'
  SUFFIX = '.acl'
  # Protocols should be emitted as numbers.
  _PROTO_INT = True
  _TERM_REMARK = True

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'address',
                         'restrict_address_family',
                         'dscp_match',
                         'icmp_code',
                         'logging',
                         'owner'}

    supported_sub_tokens.update({'option': {'established',
                                            'tcp-established',
                                            'is-fragment',
                                            'fragments'},
                                 # Warning, some of these are mapped
                                 # differently. See _ACTION_TABLE
                                 'action': {'accept', 'deny', 'reject', 'next',
                                            'reject-with-tcp-rst'}})
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.cisco_policies = []
    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    # a mixed filter outputs both ipv4 and ipv6 acls in the same output file
    good_filters = ['extended', 'standard', 'object-group', 'inet6',
                    'mixed', 'enable_dsmo']

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue
      obj_target = ObjectGroup()

      filter_options = header.FilterOptions(self._PLATFORM)
      filter_name = header.FilterName(self._PLATFORM)

      self.verbose = True
      if 'noverbose' in filter_options:
        filter_options.remove('noverbose')
        self.verbose = False

      # extended is the most common filter type.
      filter_type = 'extended'
      if len(filter_options) > 1:
        filter_type = filter_options[1]

      # check if filter type is renderable
      if filter_type not in good_filters:
        raise UnsupportedCiscoAccessListError(
            'access list type %s not supported by %s (good types: %s)' % (
                filter_type, self._PLATFORM, str(good_filters)))

      filter_list = [filter_type]
      if filter_type == 'mixed':
        # Loop through filter and generate output for inet and inet6 in sequence
        filter_list = ['extended', 'inet6']

      for next_filter in filter_list:
        # Numeric access lists can be extended or standard, but have specific
        # known ranges.
        if next_filter == 'extended' and filter_name.isdigit():
          if int(filter_name) in list(range(1, 100)) + list(range(1300, 2000)):
            raise UnsupportedCiscoAccessListError(
                'Access lists between 1-99 and 1300-1999 are reserved for '
                'standard ACLs')
        if next_filter == 'standard' and filter_name.isdigit():
          if (int(filter_name) not in list(range(1, 100)) +
              list(range(1300, 2000))):
            raise UnsupportedCiscoAccessListError(
                'Standard access lists must be numeric in the range of 1-99'
                ' or 1300-1999.')

        term_dup_check = set()
        new_terms = []
        for term in terms:
          if term.name in term_dup_check:
            raise CiscoDuplicateTermError('You have a duplicate term: %s' %
                                          term.name)
          term_dup_check.add(term.name)

          term.name = self.FixTermLength(term.name)
          af = 'inet'
          if next_filter == 'inet6':
            af = 'inet6'
          term = self.FixHighPorts(term, af=af)
          if not term:
            continue

          # Ignore if the term is for a different AF
          if term.restrict_address_family and term.restrict_address_family != af:
            continue

          if term.expiration:
            if term.expiration <= exp_info_date:
              logging.info('INFO: Term %s in policy %s expires '
                           'in less than two weeks.', term.name, filter_name)
            if term.expiration <= current_date:
              logging.warning('WARNING: Term %s in policy %s is expired and '
                              'will not be rendered.', term.name, filter_name)
              continue

          # render terms based on filter type
          if next_filter == 'standard':
            # keep track of sequence numbers across terms
            new_terms.append(TermStandard(term, filter_name, self._PLATFORM,
                                          self.verbose))
          elif next_filter == 'extended':
            enable_dsmo = (len(filter_options) > 2 and
                           filter_options[2] == 'enable_dsmo')
            new_terms.append(
                Term(term, proto_int=self._PROTO_INT, enable_dsmo=enable_dsmo,
                     term_remark=self._TERM_REMARK, platform=self._PLATFORM,
                     verbose=self.verbose))
          elif next_filter == 'object-group':
            obj_target.AddTerm(term)
            new_terms.append(self._GetObjectGroupTerm(term, filter_name,
                                                      verbose=self.verbose))
          elif next_filter == 'inet6':
            new_terms.append(
                Term(
                    term, 6, proto_int=self._PROTO_INT,
                    platform=self._PLATFORM, verbose=self.verbose))

        # cisco requires different name for the v4 and v6 acls
        if filter_type == 'mixed' and next_filter == 'inet6':
          filter_name = 'ipv6-%s' % filter_name
        self.cisco_policies.append((header, filter_name, [next_filter],
                                    new_terms, obj_target))

  def _GetObjectGroupTerm(self, term, filter_name, verbose=True):
    """Returns an ObjectGroupTerm object."""
    return ObjectGroupTerm(term, filter_name, verbose=verbose)

  def _AppendTargetByFilterType(self, filter_name, filter_type):
    """Takes in the filter name and type and appends headers.

    Args:
      filter_name: Name of the current filter
      filter_type: Type of current filter

    Returns:
      list of strings

    Raises:
      UnsupportedCiscoAccessListError: When unknown filter type is used.
    """
    target = []
    if filter_type == 'standard':
      if filter_name.isdigit():
        target.append('no access-list %s' % filter_name)
      else:
        target.append('no ip access-list standard %s' % filter_name)
        target.append('ip access-list standard %s' % filter_name)
    elif filter_type == 'extended':
      target.append('no ip access-list extended %s' % filter_name)
      target.append('ip access-list extended %s' % filter_name)
    elif filter_type == 'object-group':
      target.append('no ip access-list extended %s' % filter_name)
      target.append('ip access-list extended %s' % filter_name)
    elif filter_type == 'inet6':
      target.append('no ipv6 access-list %s' % filter_name)
      target.append('ipv6 access-list %s' % filter_name)
    else:
      raise UnsupportedCiscoAccessListError(
          'access list type %s not supported by %s' % (
              filter_type, self._PLATFORM))
    return target

  def _RepositoryTagsHelper(self, target=None, filter_type='', filter_name=''):
    if target is None:
      target = []
    if filter_type == 'standard' and filter_name.isdigit():
      target.extend(aclgenerator.AddRepositoryTags(
          'access-list %s remark ' % filter_name, date=False, revision=False))
    else:
      target.extend(aclgenerator.AddRepositoryTags(
          ' remark ', date=False, revision=False))
    return target

  def __str__(self):
    target_header = []
    target = []
    # add the p4 tags
    target.extend(aclgenerator.AddRepositoryTags('! '))

    for (header, filter_name, filter_list, terms, obj_target
        ) in self.cisco_policies:
      for filter_type in filter_list:
        target.extend(self._AppendTargetByFilterType(filter_name, filter_type))
        if filter_type == 'object-group':
          obj_target.AddName(filter_name)

        # Add the Perforce Id/Date tags, these must come after
        # remove/re-create of the filter, otherwise config mode doesn't
        # know where to place these remarks in the configuration.
        if self.verbose:
          target = self._RepositoryTagsHelper(target, filter_type, filter_name)

          # add a header comment if one exists

          for comment in aclgenerator.WrapWords(header.comment,
                                                _COMMENT_MAX_WIDTH):
            for line in comment.split('\n'):
              if (self._PLATFORM == 'cisco' and filter_type == 'standard' and
                  filter_name.isdigit()):
                target.append('access-list %s remark %s' % (filter_name, line))
              else:
                target.append(' remark %s' % line)

        # now add the terms
        for term in terms:
          term_str = str(term)
          if term_str:
            target.append(term_str)

      if obj_target.valid:
        target = [str(obj_target)] + target
      # ensure that the header is always first
      target = target_header + target
      target += ['', 'exit', '']
    return '\n'.join(target)


# capirca/lib/ciscoasa.py

# Copyright 2011 Capirca Project Authors 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.


"""Cisco ASA renderer."""

import datetime
import ipaddress
import logging
import re
from typing import cast

from capirca.lib import aclgenerator
from capirca.lib import cisco
from capirca.lib import nacaddr


_ACTION_TABLE = {
    'accept': 'permit',
    'deny': 'deny',
    'reject': 'deny',
    'next': '! next',
    'reject-with-tcp-rst': 'deny',  # tcp rst not supported
    }


# generic error class
class Error(Exception):
  """Generic error class."""
  pass


class UnsupportedCiscoAccessListError(Error):
  """Raised when we're give a non named access list."""
  pass


class StandardAclTermError(Error):
  """Raised when there is a problem in a standard access list."""
  pass


class NoCiscoPolicyError(Error):
  """Raised when a policy is errantly passed to this module for rendering."""
  pass


class Term(cisco.Term):
  """A single ACL Term."""

  def __init__(self, term, filter_name, af=4):
    self.term = term
    self.filter_name = filter_name
    self.options = []
    assert af in (4, 6)
    self.af = af

  def __str__(self):
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if 'ciscoasa' not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if 'ciscoasa' in self.term.platform_exclude:
        return ''

    ret_str = ['\n']

    # Don't render icmpv6 protocol terms under inet, or icmp under inet6
    if ((self.af == 6 and 'icmp' in self.term.protocol) or
        (self.af == 4 and 'icmpv6' in self.term.protocol)):
      ret_str.append('remark Term %s' % self.term.name)
      ret_str.append('remark not rendered due to protocol/AF mismatch.')
      return '\n'.join(ret_str)

    ret_str.append('access-list %s remark %s' % (self.filter_name,
                                                 self.term.name))
    if self.term.owner:
      self.term.comment.append('Owner: %s' % self.term.owner)
    for comment in self.term.comment:
      for line in comment.split('\n'):
        ret_str.append('access-list %s remark %s' % (self.filter_name,
                                                     str(line)[:100]))

    # Term verbatim output - this will skip over normal term creation
    # code by returning early.  Warnings provided in policy.py.
    if self.term.verbatim:
      for line in self.term.verbatim:
        if line[0] == 'ciscoasa':
          ret_str.append(str(line[1]))
        return '\n'.join(ret_str)

    # protocol
    if not self.term.protocol:
      protocol = ['ip']
    else:
      # fix the protocol
      protocol = self.term.protocol

    # source address
    if self.term.source_address:
      source_address = self.term.GetAddressOfVersion('source_address', self.af)
      source_address_exclude = self.term.GetAddressOfVersion(
          'source_address_exclude', self.af)
      if source_address_exclude:
        source_address = nacaddr.ExcludeAddrs(
            source_address,
            source_address_exclude)
    else:
      # source address not set
      source_address = ['any']

    # destination address
    if self.term.destination_address:
      destination_address = self.term.GetAddressOfVersion(
          'destination_address', self.af)
      destination_address_exclude = self.term.GetAddressOfVersion(
          'destination_address_exclude', self.af)
      if destination_address_exclude:
        destination_address = nacaddr.ExcludeAddrs(
            destination_address,
            destination_address_exclude)
    else:
      # destination address not set
      destination_address = ['any']

    # options
    extra_options = []
    for opt in [str(x) for x in self.term.option]:
      if opt.find('tcp-established') == 0 and 6 in protocol:
        extra_options.append('established')
      elif opt.find('established') == 0 and 6 in protocol:
        # only needed for TCP, for other protocols policy.py handles high-ports
        extra_options.append('established')
    self.options.extend(extra_options)

    # ports
    source_port = [()]
    destination_port = [()]
    if self.term.source_port:
      source_port = self.term.source_port
    if self.term.destination_port:
      destination_port = self.term.destination_port

    # logging
    if self.term.logging:
      self.options.append('log')
      if 'disable' in [x.value for x in self.term.logging]:
        self.options.append('disable')

    # icmp-types
    icmp_types = ['']
    if self.term.icmp_type:
      icmp_types = self.NormalizeIcmpTypes(self.term.icmp_type,
                                           self.term.protocol, self.af)

    for saddr in source_address:
      for daddr in destination_address:
        for sport in source_port:
          for dport in destination_port:
            for proto in protocol:
              for icmp_type in icmp_types:
                # only output address family appropriate IP addresses
                do_output = False
                if self.af == 4:
                  if (((isinstance(saddr, nacaddr.IPv4)) or
                       (saddr == 'any')) and
                      ((isinstance(daddr, nacaddr.IPv4)) or (daddr == 'any'))):
                    do_output = True
                if self.af == 6:
                  if (((isinstance(saddr, nacaddr.IPv6)) or
                       (saddr == 'any')) and
                      ((isinstance(daddr, nacaddr.IPv6)) or (daddr == 'any'))):
                    do_output = True
                if do_output:
                  ret_str.extend(self._TermletToStr(
                      self.filter_name,
                      _ACTION_TABLE.get(str(self.term.action[0])),
                      proto,
                      saddr,
                      sport,
                      daddr,
                      dport,
                      icmp_type,
                      self.options))

    return '\n'.join(ret_str)

  def _TermletToStr(self, filter_name, action, proto, saddr, sport, daddr,
                    dport, icmp_type, option):
    """Take the various compenents and turn them into a cisco acl line.

    Args:
      filter_name: name of the filter
      action: str, action
      proto: str, protocl
      saddr: str or ipaddress, source address
      sport: str list or none, the source port
      daddr: str or ipaddress, the destination address
      dport: str list or none, the destination port
      icmp_type: icmp-type numeric specification (if any)
      option: list or none, optional, eg. 'logging' tokens.

    Returns:
      string of the cisco acl line, suitable for printing.
    """
    # inet4
    if isinstance(saddr, nacaddr.IPv4) or isinstance(saddr,
                                                     ipaddress.IPv4Network):
      saddr = cast(self.IPV4_ADDRESS, saddr)
      if saddr.num_addresses > 1:
        saddr = '%s %s' % (saddr.network_address, saddr.netmask)
      else:
        saddr = 'host %s' % (saddr.network_address)
    if isinstance(daddr, nacaddr.IPv4) or isinstance(daddr,
                                                     ipaddress.IPv4Network):
      daddr = cast(self.IPV4_ADDRESS, daddr)
      if daddr.num_addresses > 1:
        daddr = '%s %s' % (daddr.network_address, daddr.netmask)
      else:
        daddr = 'host %s' % (daddr.network_address)
    # inet6
    if isinstance(saddr, nacaddr.IPv6) or isinstance(saddr,
                                                     ipaddress.IPv6Network):
      saddr = cast(self.IPV6_ADDRESS, saddr)
      if saddr.num_addresses > 1:
        saddr = '%s/%s' % (saddr.network_address, saddr.prefixlen)
      else:
        saddr = 'host %s' % (saddr.network_address)
    if isinstance(daddr, nacaddr.IPv6) or isinstance(daddr,
                                                     ipaddress.IPv6Network):
      daddr = cast(self.IPV6_ADDRESS, daddr)
      if daddr.num_addresses > 1:
        daddr = '%s/%s' % (daddr.network_address, daddr.prefixlen)
      else:
        daddr = 'host %s' % (daddr.network_address)

    # fix ports
    if not sport:
      sport = ''
    elif sport[0] != sport[1]:
      sport = ' range %s %s' % (cisco.PortMap.GetProtocol(sport[0], proto),
                                cisco.PortMap.GetProtocol(sport[1], proto))
    else:
      sport = ' eq %s' % (cisco.PortMap.GetProtocol(sport[0], proto))

    if not dport:
      dport = ''
    elif dport[0] != dport[1]:
      dport = ' range %s %s' % (cisco.PortMap.GetProtocol(dport[0], proto),
                                cisco.PortMap.GetProtocol(dport[1], proto))
    else:
      dport = ' eq %s' % (cisco.PortMap.GetProtocol(dport[0], proto))

    if not option:
      option = ['']

    # Prevent UDP from appending 'established' to ACL line
    sane_options = list(option)
    if proto == 'udp' and 'established' in sane_options:
      sane_options.remove('established')

    ret_lines = []

    # str(icmp_type) is needed to ensure 0 maps to '0' instead of FALSE
    icmp_type = str(cisco.PortMap.GetProtocol(icmp_type, 'icmp'))

    ret_lines.append('access-list %s extended  %s %s %s %s %s %s %s %s' %
                     (filter_name, action, proto, saddr,
                      sport, daddr, dport,
                      icmp_type,
                      ' '.join(sane_options)
                     ))

    # remove any trailing spaces and replace multiple spaces with singles
    stripped_ret_lines = [re.sub(r'\s+', ' ', x).rstrip() for x in ret_lines]
    return stripped_ret_lines


class CiscoASA(aclgenerator.ACLGenerator):
  """A cisco ASA policy object."""

  _PLATFORM = 'ciscoasa'
  _DEFAULT_PROTOCOL = 'ip'
  SUFFIX = '.asa'

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'logging', 'owner'}

    supported_sub_tokens.update({'option': {'established', 'tcp-established'},
                                 # Warning, some of these are mapped
                                 # differently. See _ACTION_TABLE
                                 'action': {'accept', 'deny', 'reject', 'next',
                                            'reject-with-tcp-rst'}})
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.ciscoasa_policies = []
    current_date = datetime.date.today()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    for header, terms in self.policy.filters:
      filter_name = header.FilterName('ciscoasa')

      new_terms = []
      # now add the terms
      for term in terms:
        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue

        new_terms.append(str(Term(term, filter_name)))

      self.ciscoasa_policies.append((header, filter_name, new_terms))

  def __str__(self):
    target = []

    for (header, filter_name, terms) in self.ciscoasa_policies:

      target.append('clear configure access-list %s' % filter_name)

      # add the p4 tags
      target.extend(aclgenerator.AddRepositoryTags('access-list %s remark '
                                                   % filter_name))

      # add a header comment if one exists
      for comment in header.comment:
        for line in comment.split('\n'):
          target.append('access-list %s remark %s' % (filter_name, line))

      # now add the terms
      for term in terms:
        target.append(str(term))

      # end for header, filter_name, filter_type...

    return '\n'.join(target)


# capirca/lib/cisconx.py

# Copyright 2021 Google 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.
#
"""CiscoNX generator."""

from capirca.lib import aclgenerator
from capirca.lib import cisco


class Error(Exception):
  """Base error class."""


class UnsupportedNXosAccessListError(Error):
  """When a filter type is not supported in an NXOS policy target."""


class CiscoNX(cisco.Cisco):
  """An CiscoNX policy object.

  CiscoNX devices differ slightly from Cisco, omitting the extended argument to
  ACLs for example.
  """

  _PLATFORM = 'cisconx'
  SUFFIX = '.nxacl'
  # Protocols should be emitted as they were in the policy (names).
  _PROTO_INT = False

  def _RepositoryTagsHelper(self, target=None, filter_type='', filter_name=''):
    if target is None:
      target = []
    target.extend(aclgenerator.AddRepositoryTags(
        ' remark ', rid=False, wrap=True))
    return target

  # CiscoNX omits the "extended" access-list argument.
  def _AppendTargetByFilterType(self, filter_name, filter_type):
    """Takes in the filter name and type and appends headers.

    Args:
      filter_name: Name of the current filter
      filter_type: Type of current filter

    Returns:
      list of strings

    Raises:
      UnsupportedNXosAccessListError: When unknown filter type is used.
    """
    target = []
    if filter_type == 'extended':
      target.append('no ip access-list %s' % filter_name)
      target.append('ip access-list %s' % filter_name)
    elif filter_type == 'object-group':
      target.append('no ip access-list %s' % filter_name)
      target.append('ip access-list %s' % filter_name)
    elif filter_type == 'inet6':
      target.append('no ipv6 access-list %s' % filter_name)
      target.append('ipv6 access-list %s' % filter_name)
    else:
      raise UnsupportedNXosAccessListError(
          'access list type %s not supported by %s' %
          (filter_type, self._PLATFORM))
    return target


# capirca/lib/ciscoxr.py

# Copyright 2011 Google 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.
#

"""Cisco IOS-XR filter renderer."""

from capirca.lib import cisco


class CiscoXR(cisco.Cisco):
  """A cisco policy object."""

  _PLATFORM = 'ciscoxr'
  _DEFAULT_PROTOCOL = 'ip'
  SUFFIX = '.xacl'
  _PROTO_INT = False

  def _AppendTargetByFilterType(self, filter_name, filter_type):
    """Takes in the filter name and type and appends headers.

    Args:
      filter_name: Name of the current filter
      filter_type: Type of current filter

    Returns:
      list of strings
    """
    target = []
    if filter_type == 'inet6':
      target.append('no ipv6 access-list %s' % filter_name)
      target.append('ipv6 access-list %s' % filter_name)
    else:
      target.append('no ipv4 access-list %s' % filter_name)
      target.append('ipv4 access-list %s' % filter_name)
    return target

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'next_ip'}

    return supported_tokens, supported_sub_tokens

  def _GetObjectGroupTerm(self, term, filter_name, verbose=True):
    """Returns an ObjectGroupTerm object."""
    return CiscoXRObjectGroupTerm(term, filter_name,
                                  platform=self._PLATFORM, verbose=verbose)


class CiscoXRObjectGroupTerm(cisco.ObjectGroupTerm):
  ALLOWED_PROTO_STRINGS = cisco.Term.ALLOWED_PROTO_STRINGS + ['pcp', 'esp']


# capirca/lib/cloudarmor.py

"""Google Cloud Armor Firewall Generator.

Refer to the links below for more information
https://cloud.google.com/armor/
https://cloud.google.com/armor/docs/
"""

import copy
import json
import logging

from capirca.lib import aclgenerator

import six


# Generic error class
class Error(Exception):
  """Generic error class."""


class ExceededMaxTermsError(Error):
  """Raised when number of terms in a policy exceed _MAX_RULES_PER_POLICY."""


class UnsupportedFilterTypeError(Error):
  """Raised when unsupported filter type (i.e address family) is specified."""


class Term(aclgenerator.Term):
  """Generates the Term for CloudArmor."""
  # Max srcIpRanges within a single term
  _MAX_IP_RANGES_PER_TERM = 5

  ACTION_MAP = {'accept': 'allow',
                'deny': 'deny(404)'}

  _MAX_TERM_COMMENT_LENGTH = 64

  def __init__(self, term, address_family='inet', verbose=True):
    super().__init__(term)
    self.term = term
    self.address_family = address_family
    self.verbose = verbose

  def __str__(self):
    return ''

  def ConvertToDict(self, priority_index):
    """Converts term to dictionary representation of CloudArmor's JSON format.

    Takes all of the attributes associated with a term (match, action, etc) and
    converts them into a dictionary which most closely represents
    the CloudArmor API's JSON rule format. Additionally, splits a single term
    into multiple terms if the number of srcIpRanges exceed
    _MAX_IP_RANGES_PER_TERM.

    Args:
      priority_index: An integer priority value assigned to the term. In case
      the term is split into i sub-terms, the ith sub-term has
      priority = priority_index + i

    Returns:
      A list of dicts where each dict is a term

    Raises:
      UnsupportedFilterTypeError: Raised when an unsupported filter type is
      specified
    """
    term_dict = {}
    rules = []

    if self.term.comment and self.verbose:
      raw_comment = ' '.join(self.term.comment)
      if len(raw_comment) > self._MAX_TERM_COMMENT_LENGTH:
        term_dict['description'] = raw_comment[:self._MAX_TERM_COMMENT_LENGTH]
        logging.warning('Term comment exceeds maximum length = %d; Truncating '
                        'comment..', self._MAX_TERM_COMMENT_LENGTH)
      else:
        term_dict['description'] = raw_comment

    term_dict['action'] = self.ACTION_MAP[self.term.action[0]]
    term_dict['preview'] = False

    if self.address_family == 'inet':
      saddrs = self.term.GetAddressOfVersion('source_address', 4)
    elif self.address_family == 'inet6':
      saddrs = self.term.GetAddressOfVersion('source_address', 6)
    elif self.address_family == 'mixed':
      saddrs = (self.term.GetAddressOfVersion('source_address', 4)
                + self.term.GetAddressOfVersion('source_address', 6))
    else:
      raise UnsupportedFilterTypeError("'%s' is not a valid filter type" %
                                       self.address_family)

    term_dict['match'] = {
        'versionedExpr': 'SRC_IPS_V1',
        'config': {
            'srcIpRanges': saddrs,
        }
    }
    # If scrIpRanges within a single term exceed _MAX_IP_RANGES_PER_TERM,
    # split into multiple terms
    source_addr_chunks = [
        saddrs[x:x+self._MAX_IP_RANGES_PER_TERM] for x in range(
            0, len(saddrs), self._MAX_IP_RANGES_PER_TERM)]

    if not source_addr_chunks:
      rule = copy.deepcopy(term_dict)
      rule['priority'] = priority_index
      rule['match']['config']['srcIpRanges'] = ['*']
      rules.append(rule)

    else:
      split_rule_count = len(source_addr_chunks)
      for i, chunk in enumerate(source_addr_chunks):
        rule = copy.deepcopy(term_dict)
        if split_rule_count > 1:
          term_position_suffix = ' [%d/%d]' % (i+1, split_rule_count)
          desc_limit = self._MAX_TERM_COMMENT_LENGTH - len(term_position_suffix)
          rule['description'] = (rule.get('description', '')[:desc_limit]
                                 + term_position_suffix)

        rule['priority'] = priority_index + i
        rule['match'] = {
            'versionedExpr': 'SRC_IPS_V1',
            'config': {
                'srcIpRanges': [str(saddr) for saddr in chunk],
            }
        }
        rules.append(rule)

    # TODO(robankeny@): Review this log entry to make it cleaner/more useful.
    # Right now, it prints the entire term which might be huge
    if len(source_addr_chunks) > 1:
      logging.debug('Current term [%s] was split into %d sub-terms since '
                    '_MAX_IP_RANGES_PER_TERM was exceeded',
                    str(term_dict), len(source_addr_chunks))
    return rules


class CloudArmor(aclgenerator.ACLGenerator):
  """A CloudArmor policy object."""

  _PLATFORM = 'cloudarmor'
  SUFFIX = '.gca'
  _SUPPORTED_AF = set(('inet', 'inet6', 'mixed'))

  # Maximum number of rules that a CloudArmor policy can contain
  _MAX_RULES_PER_POLICY = 200

  # Warn user when rule count exceeds this number
  _RULECOUNT_WARN_THRESHOLD = 190

  # Maps indiviudal filter options to their index positions in the POL header
  _FILTER_OPTIONS_MAP = {'filter_type': 0}

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, _ = super()._BuildTokens()
    supported_tokens -= {'destination_address',
                         'destination_address_exclude',
                         'destination_port',
                         'expiration',
                         'icmp_type',
                         'stateless_reply',
                         'option',
                         'protocol',
                         'platform',
                         'platform_exclude',
                         'source_address_exclude',
                         'source_port',
                         'verbatim'}
    supported_sub_tokens = {'action': {'accept', 'deny'}}
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    """Translates a Capirca policy into a CloudArmor-specific data structure.

    Takes in a POL file, parses each term and populates the cloudarmor_policies
    list. Each term in this list is a dictionary formatted according to
    CloudArmor's rule API specification.

    Args:
      pol: A Policy() object representing a given POL file.
      exp_info: An int that specifies number of weeks till policy expiry.

    Returns:
      N.A.

    Raises:
      ExceededMaxTermsError: Raised when the number of terms in a policy exceed
      _MAX_RULES_PER_POLICY.

      UnsupportedFilterTypeError: Raised when an unsupported filter type is
      specified
    """
    self.cloudarmor_policies = []

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue
      filter_options = header.FilterOptions(self._PLATFORM)
      verbose = True
      if 'noverbose' in filter_options:
        filter_options.remove('noverbose')
        verbose = False

      if filter_options is None or not filter_options:
        filter_type = 'inet'
        logging.debug('No filter_type specified. Defaulting to inet (IPv4)')

      else:
        filter_type = filter_options[self._FILTER_OPTIONS_MAP['filter_type']]
        if filter_type not in self._SUPPORTED_AF:
          raise UnsupportedFilterTypeError("'%s' is not a valid filter type" %
                                           filter_type)

      counter = 1

      for term in terms:

        json_rule_list = Term(term,
                              address_family=filter_type,
                              verbose=verbose).ConvertToDict(
                                  priority_index=counter)
        # count number of rules generated after split (if any)
        split_rule_count = len(json_rule_list)

        self.cloudarmor_policies.extend(json_rule_list)

        counter = counter + split_rule_count

        total_rule_count = len(self.cloudarmor_policies)

        if total_rule_count > self._RULECOUNT_WARN_THRESHOLD:

          if total_rule_count > self._MAX_RULES_PER_POLICY:
            raise ExceededMaxTermsError('Exceeded maximum number of rules in '
                                        ' a single policy | MAX = %d'
                                        % self._MAX_RULES_PER_POLICY)
          else:
            logging.warning('Current rule count (%d) is almost at maximum '
                            'limit of %d', total_rule_count,
                            self._MAX_RULES_PER_POLICY)

  def __str__(self):
    """Return the JSON blob for CloudArmor."""

    out = '%s\n\n' % (
        json.dumps(self.cloudarmor_policies, indent=2,
                   separators=(six.ensure_str(','), six.ensure_str(': ')),
                   sort_keys=True))
    return out


# capirca/lib/demo.py

# Copyright 2011 Google 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.
#
"""Demo generator for capirca."""

import datetime
import logging

from capirca.lib import aclgenerator


class Term(aclgenerator.Term):
  """Used to create an individual term.

     The __str__ method must be implemented.

     Args: term policy.Term object

     This is created to be a demo.
  """
  _ACTIONS = {'accept': 'allow',
              'deny': 'discard',
              'reject': 'say go away to',
              'next': 'pass it onto the next term',
              'reject-with-tcp-rst': 'reset'
             }

  def __init__(self, term, term_type):
    self.term = term
    self.term_type = term_type

  def __str__(self):
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if 'demo' not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if 'demo' in self.term.platform_exclude:
        return ''

    ret_str = []

    # NAME
    ret_str.append(' ' * 4 + 'Term: '+self.term.name+'{')

    # COMMENTS
    if self.term.comment:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + '#COMMENTS')
      for comment in self.term.comment:
        for line in comment.split('\n'):
          ret_str.append(' ' * 8 + '#'+line)

    # SOURCE ADDRESS
    source_address = self.term.GetAddressOfVersion(
        'source_address', self.AF_MAP.get(self.term_type))
    source_address_exclude = self.term.GetAddressOfVersion(
        'source_address_exclude', self.AF_MAP.get(self.term_type))
    if source_address:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + 'Source IP\'s')
      for saddr in source_address:
        ret_str.append(' ' * 8 + str(saddr))

    # SOURCE ADDRESS EXCLUDE
    if source_address_exclude:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + 'Excluded Source IP\'s')
      for ex in source_address:
        ret_str.append(' ' * 8 + str(ex))

    # SOURCE PORT
    if self.term.source_port:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + 'Source ports')
      ret_str.append(' ' * 8 + self._Group(self.term.source_port))

    # DESTINATION
    destination_address = self.term.GetAddressOfVersion(
        'destination_address', self.AF_MAP.get(self.term_type))
    destination_address_exclude = self.term.GetAddressOfVersion(
        'destination_address_exclude', self.AF_MAP.get(self.term_type))
    if destination_address:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + 'Destination IP\'s')
      for daddr in destination_address:
        ret_str.append(' ' * 8 + str(daddr))

    # DESINATION ADDRESS EXCLUDE
    if destination_address_exclude:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + 'Excluded Destination IP\'s')
      for ex in destination_address_exclude:
        ret_str.append(' ' * 8 + str(ex))

    # DESTINATION PORT
    if self.term.destination_port:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + 'Destination Ports')
      ret_str.append(' ' * 8 + self._Group(self.term.destination_port))

    # PROTOCOL
    if self.term.protocol:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + 'Protocol')
      ret_str.append(' ' * 8 + self._Group(self.term.protocol))

    # OPTION
    if self.term.option:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + 'Options')
      for option in self.term.option:
        ret_str.append(' ' * 8 + option)

    # ACTION
    for action in self.term.action:
      ret_str.append(' ')
      ret_str.append(' ' * 8 + 'Action: '
                     + self._ACTIONS.get(str(action))+' all traffic')
    return '\n '.join(ret_str)

  def _Group(self, group):
    def _FormattedGroup(el):
      if isinstance(el, str):
        return el.lower()
      elif isinstance(el, int):
        return str(el)
      elif el[0] == el[1]:
        return '%d' % el[0]
      else:
        return '%d-%d' % (el[0], el[1])
    if len(group) > 1:
      rval = ''
      for item in group:
        rval = rval + str(item[0])+' '
    else:
      rval = _FormattedGroup(group[0])
    return rval


class Demo(aclgenerator.ACLGenerator):
  """Demo rendering class.

     This class takes a policy object and renders output into
     a syntax which is not useable by routers. This class should
     only be used for testing and understanding how to create a
     generator of your own.

     Args:
       pol: policy.Policy object
     Steps to implement this library
     1) Import library in aclgen.py
     2) Create a 3 letter entry in the table in the render_filters
          function for the demo library and set it to False
     3) In the for header in policy.headers: use the previous entry
          to add an if statement to create a deep copy of the
          policy object
     4) Create an if statement that will be used if that specific
          policy object is present will pass the policy file
          onto the demo Class.
     5) The returned object can be then printed to a file using the
          do_output_filter function
     6) Create a policy file with a target set to use demo
  """
  _PLATFORM = 'demo'
  _SUFFIX = '.demo'

  _OPTIONAL_SUPPORTED_KEYWORDS = set(['expiration',])

  def _TranslatePolicy(self, pol, exp_info):
    current_date = datetime.date.today()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)
    self.demo_policies = []
    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue
      filter_options = header.FilterOptions('demo')
      filter_name = filter_options[0]
      if len(filter_options) > 1:
        interface_specific = filter_options[1]
      else:
        interface_specific = 'none'
      filter_type = 'inet'
      term_names = set()
      new_terms = []
      for term in terms:
        if term.name in term_names:
          raise DemoFilterError('Duplicate term name')
        term_names.add(term.name)
        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue
        new_terms.append(Term(term, filter_type))
      self.demo_policies.append((header, filter_name, filter_type,
                                 interface_specific, new_terms))

  def __str__(self):
    target = []
    for (header, filter_name, filter_type,
         interface_specific, terms) in self.demo_policies:
      target.append('Header {')
      target.append(' ' * 4 + 'Name: %s {' % filter_name)
      target.append(' ' * 8 + 'Type: %s ' % filter_type)
      for comment in header.comment:
        for line in comment.split('\n'):
          target.append(' ' * 8 + 'Comment: %s'%line)
      target.append(' ' * 8 + 'Family type: %s'%interface_specific)
      target.append(' ' * 4 +'}')
      for term in terms:
        target.append(str(term))
        target.append(' ' * 4 +'}')
        target.append(' ')
      target.append('}')
    return '\n'.join(target)


class Error(Exception):
  pass


class DemoFilterError(Error):
  pass


# capirca/lib/gce.py

# Copyright 2015 Google 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.
#

"""Google Compute Engine firewall generator.

More information about GCE networking and firewalls:
https://cloud.google.com/compute/docs/networking
https://cloud.google.com/compute/docs/reference/latest/firewalls
"""

import copy
import datetime
import ipaddress
import json
import logging
import re

from typing import Dict, Any

from capirca.lib import gcp
from capirca.lib import nacaddr
import six


class Error(Exception):
  """Generic error class."""


class GceFirewallError(Error):
  """Raised with problems in formatting for GCE firewall."""


class ExceededAttributeCountError(Error):
  """Raised when the total attribute count of a policy is above the maximum."""


def IsDefaultDeny(term):
  """Returns true if a term is a default deny without IPs, ports, etc."""
  skip_attrs = ['flattened', 'flattened_addr', 'flattened_saddr',
                'flattened_daddr', 'action', 'comment', 'name', 'logging']
  if 'deny' not in term.action:
    return False
  # This lc will look through all methods and attributes of the object.
  # It returns only the attributes that need to be looked at to determine if
  # this is a default deny.
  for i in [a for a in dir(term) if not a.startswith('__') and
            a.islower() and not callable(getattr(term, a))]:
    if i in skip_attrs:
      continue
    v = getattr(term, i)
    if isinstance(v, str) and v:
      return False
    if isinstance(v, list) and v:
      return False

  return True



def GetNextPriority(priority):
  """Get the priority for the next rule."""
  return priority


class Term(gcp.Term):
  """Creates the term for the GCE firewall."""

  ACTION_MAP = {'accept': 'allowed',
                'deny': 'denied'}
  # Restrict the number of addresses per term to 256.
  # Similar restrictions apply to source and target tags, and ports.
  # Details: https://cloud.google.com/vpc/docs/quota#per_network_2
  _TERM_ADDRESS_LIMIT = 256
  _TERM_SOURCE_TAGS_LIMIT = 30
  _TERM_TARGET_TAGS_LIMIT = 70
  _TERM_PORTS_LIMIT = 256

  # Firewall rule name has to match specific RE:
  # The first character must be a lowercase letter, and all following characters
  # must be a dash, lowercase letter, or digit, except the last character, which
  # cannot be a dash.
  # Details: https://cloud.google.com/compute/docs/reference/latest/firewalls
  _TERM_NAME_RE = re.compile(r'^[a-z]([-a-z0-9]*[a-z0-9])?$')

  # Protocols allowed by name from:
  # https://cloud.google.com/vpc/docs/firewalls#protocols_and_ports
  _ALLOW_PROTO_NAME = frozenset(
      ['tcp', 'udp', 'icmp', 'esp', 'ah', 'ipip', 'sctp',
       'all'  # Needed for default deny, do not use in policy file.
      ])

  # Any protocol not in _ALLOW_PROTO_NAME must be passed by number.
  ALWAYS_PROTO_NUM = set(gcp.Term.PROTO_MAP.keys()) - _ALLOW_PROTO_NAME

  def __init__(self, term, inet_version='inet', policy_inet_version='inet'):
    super().__init__(term)
    self.term = term
    self.inet_version = inet_version
    # This is to handle mixed, where the policy_inet_version is mixed,
    # but the term inet version is either inet/inet6.
    # This is only useful for term name and priority.
    self.policy_inet_version = policy_inet_version

    self._validateDirection()
    if self.term.source_address_exclude and not self.term.source_address:
      raise GceFirewallError(
          'GCE firewall does not support address exclusions without a source '
          'address list.')
    # The reason for the error below isn't because of a GCE restriction, but
    # because we don't want to use a bad default of GCE that allows talking
    # to anything when there's no source address, source tag, or source service
    # account.
    if (not self.term.source_address and
        not self.term.source_tag) and self.term.direction == 'INGRESS':
      raise GceFirewallError(
          'GCE firewall needs either to specify source address or source tags.')
    if self.term.source_port:
      raise GceFirewallError(
          'GCE firewall does not support source port restrictions.')
    if (self.term.source_address_exclude and self.term.source_address or
        self.term.destination_address_exclude and
        self.term.destination_address):
      self.term.FlattenAll()
      if not self.term.source_address and self.term.direction == 'INGRESS':
        raise GceFirewallError(
            'GCE firewall rule no longer contains any source addresses after '
            'the prefixes in source_address_exclude were removed.')
      # Similarly to the comment above, the reason for this error is also
      # because we do not want to use the bad default of GCE that allows for
      # talking to anything when there is no IP address provided for this field.
      if not self.term.destination_address and self.term.direction == 'EGRESS':
        raise GceFirewallError(
            'GCE firewall rule no longer contains any destination addresses '
            'after the prefixes in destination_address_exclude were removed.')

  def __str__(self):
    """Convert term to a string."""
    json.dumps(self.ConvertToDict(), indent=2,
               separators=(six.ensure_str(','), six.ensure_str(': ')))

  def _validateDirection(self):
    if self.term.direction == 'INGRESS':
      if not self.term.source_address and not self.term.source_tag:
        raise GceFirewallError(
            'Ingress rule missing required field oneof "sourceRanges" or '
            '"sourceTags".')

      if self.term.destination_address:
        raise GceFirewallError('Ingress rules cannot include '
                               '"destinationRanges.')

    elif self.term.direction == 'EGRESS':
      if self.term.source_address:
        raise GceFirewallError(
            'Egress rules cannot include "sourceRanges".')

      if not self.term.destination_address:
        raise GceFirewallError(
            'Egress rule missing required field "destinationRanges".')

      if self.term.destination_tag:
        raise GceFirewallError(
            'GCE Egress rule cannot have destination tag.')

  def ConvertToDict(self):
    """Convert term to a dictionary.

    This is used to get a dictionary describing this term which can be
    output easily as a JSON blob.

    Returns:
      A dictionary that contains all fields necessary to create or update a GCE
      firewall.

    Raises:
      GceFirewallError: The term name is too long.
    """
    if self.term.owner:
      self.term.comment.append('Owner: %s' % self.term.owner)
    term_dict = {
        'description': ' '.join(self.term.comment),
        'name': self.term.name,
        'direction': self.term.direction
        }
    if self.term.network:
      term_dict['network'] = self.term.network
      term_dict['name'] = '%s-%s' % (
          self.term.network.split('/')[-1], term_dict['name'])
    # Identify if this is inet6 processing for a term under a mixed policy.
    mixed_policy_inet6_term = False
    if self.policy_inet_version == 'mixed' and self.inet_version == 'inet6':
      mixed_policy_inet6_term = True
    # Update term name to have the IPv6 suffix for the inet6 rule.
    if mixed_policy_inet6_term:
      term_dict['name'] = gcp.GetIpv6TermName(term_dict['name'])

    # Checking counts of tags, and ports to see if they exceeded limits.
    if len(self.term.source_tag) > self._TERM_SOURCE_TAGS_LIMIT:
      raise GceFirewallError(
          'GCE firewall rule exceeded number of source tags per rule: %s' %
          self.term.name)
    if len(self.term.destination_tag) > self._TERM_TARGET_TAGS_LIMIT:
      raise GceFirewallError(
          'GCE firewall rule exceeded number of target tags per rule: %s' %
          self.term.name)

    if self.term.source_tag:
      if self.term.direction == 'INGRESS':
        term_dict['sourceTags'] = self.term.source_tag
      elif self.term.direction == 'EGRESS':
        term_dict['targetTags'] = self.term.source_tag
    if self.term.destination_tag and self.term.direction == 'INGRESS':
      term_dict['targetTags'] = self.term.destination_tag
    if self.term.priority:
      term_dict['priority'] = self.term.priority
      # Update term priority for the inet6 rule.
      if mixed_policy_inet6_term:
        term_dict['priority'] = GetNextPriority(term_dict['priority'])

    rules = []
    # If 'mixed' ends up in indvidual term inet_version, something has gone
    # horribly wrong. The only valid values are inet/inet6.
    term_af = self.AF_MAP.get(self.inet_version)
    if self.inet_version == 'mixed':
      raise GceFirewallError(
          'GCE firewall rule has incorrect inet_version for rule: %s' %
          self.term.name)

    # Exit early for inet6 processing of mixed rules that have only tags,
    # and no IP addresses, since this is handled in the inet processing.
    if mixed_policy_inet6_term:
      if not self.term.source_address and not self.term.destination_address:
        if 'targetTags' in term_dict or 'sourceTags' in term_dict:
          return []

    saddrs = sorted(self.term.GetAddressOfVersion('source_address', term_af),
                    key=ipaddress.get_mixed_type_key)
    daddrs = sorted(
        self.term.GetAddressOfVersion('destination_address', term_af),
        key=ipaddress.get_mixed_type_key)

    # If the address got filtered out and is empty due to address family, we
    # don't render the term. At this point of term processing, the direction
    # has already been validated, so we can just log and return empty rule.
    if self.term.source_address and not saddrs:
      logging.warning(
          'WARNING: Term %s is not being rendered for %s, '
          'because there are no addresses of that family.', self.term.name,
          self.inet_version)
      return []
    if self.term.destination_address and not daddrs:
      logging.warning(
          'WARNING: Term %s is not being rendered for %s, '
          'because there are no addresses of that family.', self.term.name,
          self.inet_version)
      return []

    filtered_protocols = []
    if not self.term.protocol:
      # Any protocol is represented as "all"
      filtered_protocols = ['all']
      logging.info(
          'INFO: Term %s has no protocol specified,'
          'which is interpreted as "all" protocols.', self.term.name)

    proto_dict = copy.deepcopy(term_dict)

    if self.term.logging:
      proto_dict['logConfig'] = {'enable': True}

    for proto in self.term.protocol:
      # ICMP filtering by inet_version
      # Since each term has inet_version, 'mixed' is correctly processed here.
      # Convert protocol to number for uniformity of comparison.
      # PROTO_MAP always returns protocol number.
      if proto in self._ALLOW_PROTO_NAME:
        proto_num = self.PROTO_MAP[proto]
      else:
        proto_num = proto
      if proto_num == self.PROTO_MAP['icmp'] and self.inet_version == 'inet6':
        logging.warning(
            'WARNING: Term %s is being rendered for inet6, ICMP '
            'protocol will not be rendered.', self.term.name)
        continue
      if proto_num == self.PROTO_MAP['icmpv6'] and self.inet_version == 'inet':
        logging.warning(
            'WARNING: Term %s is being rendered for inet, ICMPv6 '
            'protocol will not be rendered.', self.term.name)
        continue
      if proto_num == self.PROTO_MAP['igmp'] and self.inet_version == 'inet6':
        logging.warning(
            'WARNING: Term %s is being rendered for inet6, IGMP '
            'protocol will not be rendered.', self.term.name)
        continue
      filtered_protocols.append(proto)
    # If there is no protocol left after ICMP/IGMP filtering, drop this term.
    if not filtered_protocols:
      return []
    for proto in filtered_protocols:
      # If the protocol name is not supported, protocol number is used.
      # This is done by default in policy.py.
      if proto not in self._ALLOW_PROTO_NAME:
        logging.info(
            'INFO: Term %s is being rendered using protocol number',
            self.term.name)
      dest = {
          'IPProtocol': proto
          }

      if self.term.destination_port:
        ports = []
        for start, end in self.term.destination_port:
          if start == end:
            ports.append(str(start))
          else:
            ports.append('%d-%d' % (start, end))
        if len(ports) > self._TERM_PORTS_LIMIT:
          raise GceFirewallError(
              'GCE firewall rule exceeded number of ports per rule: %s' %
              self.term.name)
        dest['ports'] = ports

      action = self.ACTION_MAP[self.term.action[0]]
      dict_val = []
      if action in proto_dict:
        dict_val = proto_dict[action]
        if not isinstance(dict_val, list):
          dict_val = [dict_val]
      dict_val.append(dest)
      proto_dict[action] = dict_val

    # There's a limit of 256 addresses each term can contain.
    # If we're above that limit, we're breaking it down in more terms.
    if saddrs:
      source_addr_chunks = [
          saddrs[x:x+self._TERM_ADDRESS_LIMIT] for x in range(
              0, len(saddrs), self._TERM_ADDRESS_LIMIT)]
      for i, chunk in enumerate(source_addr_chunks):
        rule = copy.deepcopy(proto_dict)
        if len(source_addr_chunks) > 1:
          rule['name'] = '%s-%d' % (rule['name'], i+1)
        rule['sourceRanges'] = [str(saddr) for saddr in chunk]
        rules.append(rule)
    elif daddrs:
      dest_addr_chunks = [
          daddrs[x:x+self._TERM_ADDRESS_LIMIT] for x in range(
              0, len(daddrs), self._TERM_ADDRESS_LIMIT)]
      for i, chunk in enumerate(dest_addr_chunks):
        rule = copy.deepcopy(proto_dict)
        if len(dest_addr_chunks) > 1:
          rule['name'] = '%s-%d' % (rule['name'], i+1)
        rule['destinationRanges'] = [str(daddr) for daddr in chunk]
        rules.append(rule)
    else:
      rules.append(proto_dict)

    # Sanity checking term name lengths.
    long_rules = [rule['name'] for rule in rules if len(rule['name']) > 63]
    if long_rules:
      raise GceFirewallError(
          'GCE firewall name ended up being too long: %s' % long_rules)
    return rules


class GCE(gcp.GCP):
  """A GCE firewall policy object."""

  _PLATFORM = 'gce'
  SUFFIX = '.gce'
  _SUPPORTED_AF = frozenset(('inet', 'inet6', 'mixed'))
  _ANY_IP = {
      'inet': nacaddr.IP('0.0.0.0/0'),
      'inet6': nacaddr.IP('::/0'),
  }
  # Supported is 63 but we need to account for dynamic updates when the term
  # is rendered (which can add proto and a counter).
  _TERM_MAX_LENGTH = 53
  _GOOD_DIRECTION = ['INGRESS', 'EGRESS']
  _OPTIONAL_SUPPORTED_KEYWORDS = set(['expiration',
                                      'destination_tag',
                                      'source_tag'])

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, _ = super()._BuildTokens()

    # add extra things
    supported_tokens |= {'destination_tag',
                         'expiration',
                         'owner',
                         'priority',
                         'source_tag'}

    # remove unsupported things
    supported_tokens -= {'icmp_type',
                         'verbatim'}
    # easier to make a new structure
    supported_sub_tokens = {'action': {'accept', 'deny'}}

    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.gce_policies = []
    max_attribute_count = 0
    total_attribute_count = 0
    total_rule_count = 0

    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)
      filter_name = header.FilterName(self._PLATFORM)

      network = ''
      direction = 'INGRESS'
      if filter_options:
        for i in self._GOOD_DIRECTION:
          if i in filter_options:
            direction = i
            filter_options.remove(i)
      # Get the address family if set.
      address_family = 'inet'
      for i in self._SUPPORTED_AF:
        if i in filter_options:
          address_family = i
          filter_options.remove(i)

      for opt in filter_options:
        try:
          max_attribute_count = int(opt)
          logging.info(
              'Checking policy for max attribute count %d', max_attribute_count)
          filter_options.remove(opt)
          break
        except ValueError:
          continue

      if filter_options:
        network = filter_options[0]
      else:
        logging.warning('GCE filter does not specify a network.')

      term_names = set()
      if IsDefaultDeny(terms[-1]):
        terms[-1].protocol = ['all']
        terms[-1].priority = 65534
        if direction == 'EGRESS':
          if address_family != 'mixed':
            # Default deny also gets processed as part of terms processing.
            # The name and priority get updated there.
            terms[-1].destination_address = [self._ANY_IP[address_family]]
          else:
            terms[-1].destination_address = [self._ANY_IP['inet'],
                                             self._ANY_IP['inet6']]
        else:
          if address_family != 'mixed':
            terms[-1].source_address = [self._ANY_IP[address_family]]
          else:
            terms[-1].source_address = [
                self._ANY_IP['inet'], self._ANY_IP['inet6']
            ]

      for term in terms:
        if term.stateless_reply:
          logging.warning('WARNING: Term %s in policy %s is a stateless reply '
                          'term and will not be rendered.',
                          term.name, filter_name)
          continue
        term.network = network
        if not term.comment:
          term.comment = header.comment
        if direction == 'EGRESS':
          term.name += '-e'
        term.name = self.FixTermLength(term.name)
        if term.name in term_names:
          raise GceFirewallError('Duplicate term name %s' % term.name)
        term_names.add(term.name)

        term.direction = direction
        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue
        if term.option:
          raise GceFirewallError(
              'GCE firewall does not support term options.')

        # Only generate the term if it's for the appropriate platform
        if term.platform:
          if self._PLATFORM not in term.platform:
            continue
        if term.platform_exclude:
          if self._PLATFORM in term.platform_exclude:
            continue

        # Handle mixed for each indvidual term as inet and inet6.
        # inet/inet6 are treated the same.
        term_address_families = []
        if address_family == 'mixed':
          term_address_families = ['inet', 'inet6']
        else:
          term_address_families = [address_family]
        for term_af in term_address_families:
          for rules in Term(term, term_af, address_family).ConvertToDict():
            logging.debug('Attribute count of rule %s is: %d', term.name,
                          GetAttributeCount(rules))
            total_attribute_count += GetAttributeCount(rules)
            total_rule_count += 1
            if max_attribute_count and total_attribute_count > max_attribute_count:
              # Stop processing rules as soon as the attribute count is over the
              # limit.
              raise ExceededAttributeCountError(
                  'Attribute count (%d) for %s exceeded the maximum (%d)' %
                  (total_attribute_count, filter_name, max_attribute_count))
            self.gce_policies.append(rules)
    logging.info('Total rule count of policy %s is: %d', filter_name,
                 total_rule_count)
    logging.info('Total attribute count of policy %s is: %d', filter_name,
                 total_attribute_count)

  def __str__(self):
    out = '%s\n\n' % (json.dumps(self.gce_policies, indent=2,
                                 separators=(six.ensure_str(','),
                                             six.ensure_str(': ')),
                                 sort_keys=True))

    return out


def GetAttributeCount(dict_term: Dict[str, Any]) -> int:
  """Calculate the attribute count of a term in its dictionary form.

  The attribute count of a rule is the sum of the number of ports, protocols, IP
  ranges, tags and target service account.

  Note: The goal of this function is not to determine if a term is valid, but
      to calculate its attribute count regardless of correctness.

  Args:
    dict_term: A dict object.

  Returns:
    int: The attribute count of the term.
  """
  addresses = (len(dict_term.get('destinationRanges', []))
               or len(dict_term.get('sourceRanges', [])))

  proto_ports = 0
  for allowed in dict_term.get('allowed', []):
    proto_ports += len(allowed.get('ports', [])) + 1  # 1 for ipProtocol
  for denied in dict_term.get('denied', []):
    proto_ports += len(denied.get('ports', [])) + 1  # 1 for ipProtocol

  tags = 0
  for _ in dict_term.get('sourceTags', []):
    tags += 1
  for _ in dict_term.get('targetTags', []):
    tags += 1

  service_accounts = 0
  for _ in dict_term.get('targetServiceAccount', []):
    service_accounts += 1

  return addresses + proto_ports + tags + service_accounts


# capirca/lib/gcp.py

"""Generic Google Cloud Platform multi-product generator.

Base class for GCP firewalling products.
"""

import json
import re

from capirca.lib import aclgenerator

import six


class Error(Exception):
  """Generic error class."""


class TermError(Error):
  """Raised when a term is not valid."""


class HeaderError(Error):
  """Raised when a header is not valid."""


class UnsupportedFilterTypeError(Error):
  """Raised when an unsupported filter type is specified."""


class Term(aclgenerator.Term):
  """A Term object."""

  # Protocols allowed by name from:
  # https://cloud.google.com/vpc/docs/firewalls#protocols_and_ports
  # 'all' is needed for the dedault deny, it should not be used in a pol file.
  _ALLOW_PROTO_NAME = frozenset(
      ['tcp', 'udp', 'icmp', 'esp', 'ah', 'ipip', 'sctp',
       'all'
      ])

  def _GetPorts(self):
    """Return a port or port range in string format."""
    ports = []
    for start, end in self.term.destination_port:
      if start == end:
        ports.append(str(start))
      else:
        ports.append('%d-%d' % (start, end))
    return ports

  def _GetLoggingSetting(self):
    """Return true if a term indicates that logging is desired."""
    # Supported values in GCP are '', 'true', and 'True'.
    settings = [str(x) for x in self.term.logging]
    if any(value in settings for value in ['true', 'True']):
      return True
    return False


class GCP(aclgenerator.ACLGenerator):
  """A GCP object."""

  policies = []
  _GOOD_DIRECTION = ['INGRESS', 'EGRESS']

  def __str__(self):
    """Return the JSON blob for a GCP object."""
    out = '%s\n\n' % (
        json.dumps(
            self.policies,
            indent=2,
            separators=(six.ensure_str(','), six.ensure_str(': ')),
            sort_keys=True))
    return out


def IsDefaultDeny(term):
  """Return true if a term is a default deny without IPs, ports, etc."""
  skip_attrs = [
      'flattened', 'flattened_addr', 'flattened_saddr', 'flattened_daddr',
      'action', 'comment', 'name', 'logging'
  ]
  if 'deny' not in term.action:
    return False
  # This lc will look through all methods and attributes of the object.
  # It returns only the attributes that need to be looked at to determine if
  # this is a default deny.
  for i in [
      a for a in dir(term) if not a.startswith('__') and a.islower() and
      not callable(getattr(term, a))
  ]:
    if i in skip_attrs:
      continue
    v = getattr(term, i)
    if isinstance(v, str) and v:
      return False
    if isinstance(v, list) and v:
      return False

  return True


def IsProjectIDValid(project):
  """Return true if a project ID is valid.

  https://cloud.google.com/resource-manager/reference/rest/v1/projects

  "It must be 6 to 30 lowercase letters, digits, or hyphens. It must start with
  a letter. Trailing hyphens are prohibited."

  Args:
    project: A string.

  Returns:
    bool: True if a project ID matches the pattern and length requirements.
  """
  if len(project) < 6 or len(project) > 30:
    return False
  return bool(re.match('^[a-z][a-z0-9\\-]*[a-z0-9]$', project))


def IsVPCNameValid(vpc):
  """Return true if a VPC name is valid.

  https://cloud.google.com/compute/docs/reference/rest/v1/networks

  "The first character must be a lowercase letter, and all following characters
  (except for the last character) must be a dash, lowercase letter, or digit.
  The last character must be a lowercase letter or digit."

  Args:
    vpc: A string.

  Returns:
    bool: True if a VPC name matches the pattern and length requirements.
  """
  if len(vpc) < 1 or len(vpc) > 63:
    return False
  return bool(re.match('^[a-z]$|^[a-z][a-z0-9-]*[a-z0-9]$', vpc))


def TruncateString(raw_string, max_length):
  """Returns truncated raw_string based on max length.

  Args:
    raw_string: String to be truncated.
    max_length: max length of string.

  Returns:
    string: The truncated string.
  """
  if len(raw_string) > max_length:
    return raw_string[:max_length]
  return raw_string


def GetIpv6TermName(term_name):
  """Returns the equivalent term name for IPv6 terms.

  Args:
    term_name: A string.

  Returns:
    string: The IPv6 requivalent term name.
  """

  return '%s-%s' % (term_name, 'v6')


# capirca/lib/gcp_hf.py

"""Google Cloud Hierarchical Firewall Generator.

Hierarchical Firewalls (HF) are represented in a SecurityPolicy GCP resouce.
"""

import copy
import re

from typing import Dict, Any

from absl import logging
from capirca.lib import gcp
from capirca.lib import nacaddr


class ExceededCostError(gcp.Error):
  """Raised when the total cost of a policy is above the maximum."""


class DifferentPolicyNameError(gcp.Error):
  """Raised when headers in the same policy have a different policy name."""


class ApiVersionSyntaxMap:
  """Defines the syntax changes between different API versions.

   http://cloud/compute/docs/reference/rest/v1/firewallPolicies/addRule
   http://cloud/compute/docs/reference/rest/beta/organizationSecurityPolicies/addRule
  """
  SYNTAX_MAP = {
      'beta': {
          'display_name': 'displayName',
          'dest_ip_range': 'destIpRanges',
          'src_ip_range': 'srcIpRanges',
          'layer_4_config': 'layer4Configs'
      },
      'ga': {
          'display_name': 'shortName',
          'dest_ip_range': 'destIpRanges',
          'src_ip_range': 'srcIpRanges',
          'layer_4_config': 'layer4Configs'
      }
  }


class Term(gcp.Term):
  """Used to create an individual term."""

  ACTION_MAP = {'accept': 'allow', 'next': 'goto_next'}

  _MAX_TERM_COMMENT_LENGTH = 64

  _TARGET_RESOURCE_FORMAT = 'https://www.googleapis.com/compute/v1/projects/{}/global/networks/{}'

  _TERM_ADDRESS_LIMIT = 256

  _TERM_TARGET_RESOURCES_LIMIT = 256

  _TERM_DESTINATION_PORTS_LIMIT = 256

  def __init__(self,
               term,
               address_family='inet',
               policy_inet_version='inet',
               api_version='beta'):
    super().__init__(term)
    self.address_family = address_family
    self.term = term
    self.skip = False
    self._ValidateTerm()
    self.api_version = api_version

    # This is to handle mixed, where the policy_inet_version is mixed,
    # but the term inet version is either inet/inet6.
    # This is only useful for term name and priority.
    self.policy_inet_version = policy_inet_version

  def _ValidateTerm(self):
    if self.term.destination_tag or self.term.source_tag:
      raise gcp.TermError('Hierarchical Firewall does not support tags')

    if len(self.term.target_resources) > self._TERM_TARGET_RESOURCES_LIMIT:
      raise gcp.TermError(
          'Term: %s  target_resources field contains %s resources. It should not contain more than "%s".'
          % (self.term.name, str(len(
              self.term.target_resources)), self._TERM_TARGET_RESOURCES_LIMIT))

    for proj, vpc in self.term.target_resources:
      if not gcp.IsProjectIDValid(proj):
        raise gcp.TermError(
            'Project ID "%s" must be 6 to 30 lowercase letters, digits, or hyphens.'
            ' It must start with a letter. Trailing hyphens are prohibited.' %
            proj)
      if not gcp.IsVPCNameValid(vpc):
        raise gcp.TermError('VPC name "%s" must start with a lowercase letter '
                            'followed by up to 62 lowercase letters, numbers, '
                            'or hyphens, and cannot end with a hyphen.' % vpc)
    if self.term.source_port:
      raise gcp.TermError('Hierarchical firewall does not support source port '
                          'restrictions.')
    if self.term.option:
      raise gcp.TermError('Hierarchical firewall does not support the '
                          'TCP_ESTABLISHED option.')

    if len(self.term.destination_port) > self._TERM_DESTINATION_PORTS_LIMIT:
      raise gcp.TermError(
          'Term: %s destination_port field contains %s ports. It should not contain more than "%s".'
          % (self.term.name, str(len(
              self.term.destination_port)), self._TERM_DESTINATION_PORTS_LIMIT))

    # Since policy_inet_version is used to handle 'mixed'.
    # We should error out if the individual term's inet version (address_family)
    # is anything other than inet/inet6, since this should never happen
    # naturally. Something has gone horribly wrong if you encounter this error.
    if self.address_family == 'mixed':
      raise gcp.TermError(
          'Hierarchical firewall rule has incorrect inet_version for rule: %s' %
          self.term.name)

  def ConvertToDict(self, priority_index):
    """Converts term to dict representation of SecurityPolicy.Rule JSON format.

    Takes all of the attributes associated with a term (match, action, etc) and
    converts them into a dictionary which most closely represents
    the SecurityPolicy.Rule JSON format.

    Args:
      priority_index: An integer priority value assigned to the term.

    Returns:
      A dict term.
    """
    if self.skip:
      return {}

    rules = []

    # Identify if this is inet6 processing for a term under a mixed policy.
    mixed_policy_inet6_term = False
    if self.policy_inet_version == 'mixed' and self.address_family == 'inet6':
      mixed_policy_inet6_term = True

    term_dict = {
        'action': self.ACTION_MAP.get(self.term.action[0], self.term.action[0]),
        'direction': self.term.direction,
        'priority': priority_index
    }

    # Get the correct syntax for API versions.
    src_ip_range = ApiVersionSyntaxMap.SYNTAX_MAP[
        self.api_version]['src_ip_range']
    dest_ip_range = ApiVersionSyntaxMap.SYNTAX_MAP[
        self.api_version]['dest_ip_range']
    layer_4_config = ApiVersionSyntaxMap.SYNTAX_MAP[
        self.api_version]['layer_4_config']

    target_resources = []
    for proj, vpc in self.term.target_resources:
      target_resources.append(self._TARGET_RESOURCE_FORMAT.format(proj, vpc))

    if target_resources:  # Only set when non-empty.
      term_dict['targetResources'] = target_resources

    term_dict['enableLogging'] = self._GetLoggingSetting()

    # This combo provides ability to identify the rule.
    term_name = self.term.name
    if mixed_policy_inet6_term:
      term_name = gcp.GetIpv6TermName(term_name)
    raw_description = term_name + ': ' + ' '.join(self.term.comment)
    term_dict['description'] = gcp.TruncateString(raw_description,
                                                  self._MAX_TERM_COMMENT_LENGTH)

    filtered_protocols = []
    for proto in self.term.protocol:
      # ICMP filtering by inet_version
      # Since each term has inet_version, 'mixed' is correctly processed here.
      if proto == 'icmp' and self.address_family == 'inet6':
        logging.warning(
            'WARNING: Term %s is being rendered for inet6, ICMP '
            'protocol will not be rendered.', self.term.name)
        continue
      if proto == 'icmpv6' and self.address_family == 'inet':
        logging.warning(
            'WARNING: Term %s is being rendered for inet, ICMPv6 '
            'protocol will not be rendered.', self.term.name)
        continue
      if proto == 'igmp' and self.address_family == 'inet6':
        logging.warning(
            'WARNING: Term %s is being rendered for inet6, IGMP '
            'protocol will not be rendered.', self.term.name)
        continue
      filtered_protocols.append(proto)
    # If there is no protocol left after ICMP/IGMP filtering, drop this term.
    # But only do this for terms that originally had protocols.
    # Otherwise you end up dropping the default-deny.
    if self.term.protocol and not filtered_protocols:
      return {}

    protocols_and_ports = []
    if not self.term.protocol:
      # Empty protocol list means any protocol, but any protocol in HF is
      # represented as "all"
      protocols_and_ports = [{'ipProtocol': 'all'}]
    else:
      for proto in filtered_protocols:
        # If the protocol name is not supported, use the protocol number.
        if proto not in self._ALLOW_PROTO_NAME:
          proto = str(self.PROTO_MAP[proto])
          logging.info('INFO: Term %s is being rendered using protocol number',
                       self.term.name)
        proto_ports = {'ipProtocol': proto}
        if self.term.destination_port:
          ports = self._GetPorts()
          if ports:  # Only set when non-empty.
            proto_ports['ports'] = ports
        protocols_and_ports.append(proto_ports)

    if self.api_version == 'ga':
      term_dict['match'] = {layer_4_config: protocols_and_ports}
    else:
      term_dict['match'] = {'config': {layer_4_config: protocols_and_ports}}

    # match needs a field called versionedExpr with value FIREWALL
    # See documentation:
    # https://cloud.google.com/compute/docs/reference/rest/beta/organizationSecurityPolicies/addRule
    term_dict['match']['versionedExpr'] = 'FIREWALL'

    ip_version = self.AF_MAP[self.address_family]
    if ip_version == 4:
      any_ip = [nacaddr.IP('0.0.0.0/0')]
    else:
      any_ip = [nacaddr.IPv6('::/0')]

    if self.term.direction == 'EGRESS':
      daddrs = self.term.GetAddressOfVersion('destination_address', ip_version)

      # If the address got filtered out and is empty due to address family, we
      # don't render the term. At this point of term processing, the direction
      # has already been validated, so we can just log and return empty rule.
      if self.term.destination_address and not daddrs:
        logging.warning(
            'WARNING: Term %s is not being rendered for %s, '
            'because there are no addresses of that family.', self.term.name,
            self.address_family)
        return []
      # This should only happen if there were no addresses set originally.
      if not daddrs:
        daddrs = any_ip

      destination_address_chunks = [
          daddrs[x:x + self._TERM_ADDRESS_LIMIT]
          for x in range(0, len(daddrs), self._TERM_ADDRESS_LIMIT)
      ]

      for daddr_chunk in destination_address_chunks:
        rule = copy.deepcopy(term_dict)
        if self.api_version == 'ga':
          rule['match'][dest_ip_range] = [
              daddr.with_prefixlen for daddr in daddr_chunk
          ]
        else:
          rule['match']['config'][dest_ip_range] = [
              daddr.with_prefixlen for daddr in daddr_chunk
          ]
        rule['priority'] = priority_index
        rules.append(rule)
        priority_index += 1
    else:
      saddrs = self.term.GetAddressOfVersion('source_address', ip_version)

      # If the address got filtered out and is empty due to address family, we
      # don't render the term. At this point of term processing, the direction
      # has already been validated, so we can just log and return empty rule.
      if self.term.source_address and not saddrs:
        logging.warning(
            'WARNING: Term %s is not being rendered for %s, '
            'because there are no addresses of that family.', self.term.name,
            self.address_family)
        return []
      # This should only happen if there were no addresses set originally.
      if not saddrs:
        saddrs = any_ip

      source_address_chunks = [
          saddrs[x:x + self._TERM_ADDRESS_LIMIT]
          for x in range(0, len(saddrs), self._TERM_ADDRESS_LIMIT)
      ]
      for saddr_chunk in source_address_chunks:
        rule = copy.deepcopy(term_dict)
        if self.api_version == 'ga':
          rule['match'][src_ip_range] = [
              saddr.with_prefixlen for saddr in saddr_chunk
          ]
        else:
          rule['match']['config'][src_ip_range] = [
              saddr.with_prefixlen for saddr in saddr_chunk
          ]
        rule['priority'] = priority_index
        rules.append(rule)
        priority_index += 1

    return rules

  def __str__(self):
    return ''


class HierarchicalFirewall(gcp.GCP):
  """A GCP Hierarchical Firewall policy."""

  SUFFIX = '.gcphf'
  _ANY_IP = {
      'inet': nacaddr.IP('0.0.0.0/0'),
      'inet6': nacaddr.IP('::/0'),
  }
  _PLATFORM = 'gcp_hf'
  _SUPPORTED_AF = frozenset(['inet', 'inet6', 'mixed'])
  # Beta is the default API version. GA supports IPv6 (inet6/mixed).
  _SUPPORTED_API_VERSION = frozenset(['beta', 'ga'])
  _DEFAULT_MAXIMUM_COST = 100

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      Tuple containing both supported tokens and sub tokens.
    """
    supported_tokens, _ = super()._BuildTokens()

    supported_tokens |= {
        'destination_tag', 'expiration', 'source_tag', 'translated',
        'target_resources', 'logging'
    }

    supported_tokens -= {
        'destination_address_exclude', 'expiration', 'icmp_type',
        'source_address_exclude', 'verbatim'
    }

    supported_sub_tokens = {'action': {'accept', 'deny', 'next'}}
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    """Translates a Capirca policy into a HF-specific data structure.

    Takes in a POL file, parses each term and populates the policy
    dict. Each term in this list is a dictionary formatted according to
    HF's rule API specification.  Additionally, checks for its quota.

    Args:
      pol: A Policy() object representing a given POL file.
      exp_info: An int that specifies number of weeks until policy expiry.

    Raises:
      ExceededCostError: Raised when the cost of a policy exceeds the default
          maximum cost.
      HeaderError: Raised when the header cannot be parsed or a header option is
          invalid.
      DifferentPolicyNameError: Raised when a header policy name differs from
          other in the same policy.
    """
    self.policies = []
    policy = {
        'rules': [],
        'type': 'FIREWALL'
    }
    is_policy_modified = False
    counter = 1
    total_cost = 0
    policies_max_cost = self._DEFAULT_MAXIMUM_COST
    previous_max_cost = -1
    for header, terms in pol.filters:

      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)

      is_policy_modified = True

      # Get term direction if set.
      direction = 'INGRESS'
      for i in self._GOOD_DIRECTION:
        if i in filter_options:
          direction = i
          filter_options.remove(i)

      # Get the address family if set.
      address_family = 'inet'
      for i in self._SUPPORTED_AF:
        if i in filter_options:
          address_family = i
          filter_options.remove(i)

      # Get the compute API version if set.
      api_version = 'beta'
      for i in self._SUPPORTED_API_VERSION:
        if i in filter_options:
          api_version = i
          filter_options.remove(i)
          break

      # Find the default maximum cost of a policy, an integer, if specified.
      max_cost = self._DEFAULT_MAXIMUM_COST
      for opt in filter_options:
        try:
          max_cost = int(opt)
          filter_options.remove(opt)
          break
        except ValueError:
          continue

      if max_cost > 65536:
        raise gcp.HeaderError(
            'Default maximum cost cannot be higher than 65536')

      if previous_max_cost != -1 and previous_max_cost != max_cost:
        raise gcp.HeaderError(
            'Maximum costs of each policy specified must be equal. '
            'Unequal costs found: %d and %d' % (previous_max_cost, max_cost))

      policies_max_cost = max_cost
      previous_max_cost = max_cost

      display_name = ApiVersionSyntaxMap.SYNTAX_MAP[api_version]['display_name']

      # Get policy name and validate it to meet displayName requirements.
      policy_name = header.FilterName(self._PLATFORM)
      if not policy_name:
        raise gcp.HeaderError(
            'Policy name was not specified in header')
      filter_options.remove(policy_name)
      if len(policy_name) > 63:
        raise gcp.HeaderError(
            'Policy name "%s" is too long; the maximum number of characters '
            'allowed is 63' % (policy_name))
      if not bool(re.match('^[a-z]([-a-z0-9]*[a-z0-9])?$', policy_name)):
        raise gcp.HeaderError(
            'Invalid string for displayName, "%s"; the first character must be '
            'a lowercase letter, and all following characters must be a dash, '
            'lowercase letter, or digit, except the last character, which '
            'cannot be a dash.' % (policy_name))
      if display_name in policy and policy[display_name] != policy_name:
        raise DifferentPolicyNameError(
            'Policy names that are from the same policy are expected to be '
            'equal, but %s is different to %s' %
            (policy[display_name], policy_name))
      policy[display_name] = policy_name

      # If there are remaining options, they are unknown/unsupported options.
      if filter_options:
        raise gcp.HeaderError(
            'Unsupported or unknown filter options %s in policy %s ' %
            (str(filter_options), policy_name))

      # Handle mixed for each indvidual term as inet and inet6.
      # inet/inet6 are treated the same.
      term_address_families = []
      if address_family == 'mixed':
        term_address_families = ['inet', 'inet6']
      else:
        term_address_families = [address_family]

      for term in terms:
        if term.stateless_reply:
          continue

        if gcp.IsDefaultDeny(term):
          if direction == 'EGRESS':
            if address_family != 'mixed':
              # Default deny also gets processed as part of terms processing.
              # The name and priority get updated there.
              term.destination_address = [self._ANY_IP[address_family]]
            else:
              term.destination_address = [
                  self._ANY_IP['inet'], self._ANY_IP['inet6']
              ]
          else:
            if address_family != 'mixed':
              term.source_address = [self._ANY_IP[address_family]]
            else:
              term.source_address = [
                  self._ANY_IP['inet'], self._ANY_IP['inet6']
              ]
        term.name = self.FixTermLength(term.name)
        term.direction = direction

        # Only generate the term if it's for the appropriate platform
        if term.platform:
          if self._PLATFORM not in term.platform:
            continue
        if term.platform_exclude:
          if self._PLATFORM in term.platform_exclude:
            continue

        for term_af in term_address_families:
          rules = Term(
              term,
              address_family=term_af,
              policy_inet_version=address_family,
              api_version=api_version).ConvertToDict(priority_index=counter)
          if not rules:
            continue
          for dict_term in rules:
            total_cost += GetRuleTupleCount(dict_term, api_version)
            policy['rules'].append(dict_term)
          counter += len(rules)

    # We want to check the total policy cost, not just per policy.
    if total_cost > policies_max_cost:
      raise ExceededCostError(
          'Policy cost (%d) for %s reached the '
          'maximum (%d)' %
          (total_cost, policy[display_name], policies_max_cost))

    self.policies.append(policy)

    # Do not render an empty rules if no policies have been evaluated.
    if not is_policy_modified:
      self.policies = []

    if total_cost > 0:
      logging.info('Policy %s quota cost: %d',
                   policy[display_name], total_cost)


def GetRuleTupleCount(dict_term: Dict[str, Any], api_version):
  """Calculate the tuple count of a rule in its dictionary form.

  Quota is charged based on how complex the rules are rather than simply
  limiting the number of rules.

  The cost of a rule is the number of distinct protocol:port combinations plus
  the number of IP addresses plus the number of targets.

  Note: The goal of this function is not to determine if a rule is valid, but
      to calculate its tuple count regardless of correctness.

  Args:
    dict_term: A dict object.
    api_version: A string indicating the api version.

  Returns:
    int: The tuple count of the rule.
  """
  layer4_count = 0
  layer_4_config = ApiVersionSyntaxMap.SYNTAX_MAP[api_version]['layer_4_config']
  dest_ip_range = ApiVersionSyntaxMap.SYNTAX_MAP[api_version]['dest_ip_range']
  src_ip_range = ApiVersionSyntaxMap.SYNTAX_MAP[api_version]['src_ip_range']
  targets_count = len(dict_term.get('targetResources', []))
  if api_version == 'ga':
    config = dict_term.get('match', {})
  else:
    config = dict_term.get('match', {}).get('config', {})

  addresses_count = len(
      config.get(dest_ip_range, []) + config.get(src_ip_range, []))

  for l4config in config.get(layer_4_config, []):
    for _ in l4config.get('ports', []):
      layer4_count += 1
    if l4config.get('ipProtocol'):
      layer4_count += +1

  return addresses_count + layer4_count + targets_count


# capirca/lib/ipset.py

# Copyright 2015 Google 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.
#
"""Ipset iptables generator.  This is a subclass of Iptables generator.

ipset is a system inside the Linux kernel, which can very efficiently store
and match IPv4 and IPv6 addresses. This can be used to dramatically increase
performace of iptables firewall.

"""

import string

from capirca.lib import iptables
from capirca.lib import nacaddr


class Error(Exception):
  """Base error class."""


class Term(iptables.Term):
  """Single Ipset term representation."""

  _PLATFORM = 'ipset'
  _SET_MAX_LENGTH = 31
  _POSTJUMP_FORMAT = None
  _PREJUMP_FORMAT = None
  _TERM_FORMAT = None
  _COMMENT_FORMAT = string.Template(
      '-A $filter -m comment --comment "$comment"')
  _FILTER_TOP_FORMAT = string.Template('-A $filter')

  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    # This stores tuples of set name and set contents, keyed by direction.
    # For example:
    # { 'src': ('set_name', [ipaddr object, ipaddr object]),
    #   'dst': ('set_name', [ipaddr object, ipaddr object]) }
    self.addr_sets = {}

  def _CalculateAddresses(self, src_addr_list, src_addr_exclude_list,
                          dst_addr_list, dst_addr_exclude_list):
    """Calculates source and destination address list for a term.

    Since ipset is very efficient at matching large number of
    addresses, we never return any exclude addresses. Instead
    least positive match is calculated for both source and destination
    addresses.

    For source and destination address list, three cases are possible.
    First case is when there are no addresses. In that case we return
    _all_ips.
    Second case is when there is strictly one address. In that case,
    we optimize by not generating a set, and it's then the only
    element of returned set.
    Third case is when there are more than one address in a set.
    In that case we generate a set and also return _all_ips. Note the
    difference to the first case where no set is actually generated.

    Args:
      src_addr_list: source address list of the term.
      src_addr_exclude_list: source address exclude list of the term.
      dst_addr_list: destination address list of the term.
      dst_addr_exclude_list: destination address exclude list of the term.

    Returns:
      tuple containing source address list, source address exclude list,
      destination address list, destination address exclude list in
      that order.

    """
    target_af = self.AF_MAP[self.af]
    src_addr_list = self._CalculateAddrList(src_addr_list,
                                            src_addr_exclude_list, target_af,
                                            'src')
    dst_addr_list = self._CalculateAddrList(dst_addr_list,
                                            dst_addr_exclude_list, target_af,
                                            'dst')
    return (src_addr_list, [], dst_addr_list, [])

  def _CalculateAddrList(self, addr_list, addr_exclude_list,
                         target_af, direction):
    """Calculates and stores address list for target AF and direction.

    Args:
      addr_list: address list.
      addr_exclude_list: address exclude list of the term.
      target_af: target address family.
      direction: direction in which address list will be used.

    Returns:
      calculated address list.

    """
    if not addr_list:
      addr_list = [self._all_ips]
    addr_list = [addr for addr in addr_list if addr.version == target_af]
    if addr_exclude_list:
      addr_exclude_list = [addr_exclude for addr_exclude in addr_exclude_list if
                           addr_exclude.version == target_af]
      addr_list = nacaddr.ExcludeAddrs(addr_list, addr_exclude_list)
    if len(addr_list) > 1:
      set_name = self._GenerateSetName(self.term.name, direction)
      self.addr_sets[direction] = (set_name, addr_list)
      addr_list = [self._all_ips]
    return addr_list

  def _GenerateAddressStatement(self, src_addr, dst_addr):
    """Returns the address section of an individual iptables rule.

    See _CalculateAddresses documentation. Three cases are possible here,
    and they map directly to cases in _CalculateAddresses.
    First, there can be no addresses for a direction (value is _all_ips then)
    In that case we return empty string.
    Second there can be stricly one address. In that case we return single
    address match (-s or -d).
    Third case, is when the value is _all_ips but also the set for particular
    direction is present. That's when we return a set match.

    Args:
      src_addr: ipaddr address or network object with source
        address of the rule.
      dst_addr: ipaddr address or network object with destination
        address of the rule.

    Returns:
      tuple containing source and destination address statement, in
      that order.

    """
    src_addr_stmt = ''
    dst_addr_stmt = ''
    if src_addr and dst_addr:
      if src_addr == self._all_ips:
        if 'src' in self.addr_sets:
          src_addr_stmt = ('-m set --match-set %s src' %
                           self.addr_sets['src'][0])
      else:
        src_addr_stmt = '-s %s/%d' % (src_addr.network_address,
                                      src_addr.prefixlen)
      if dst_addr == self._all_ips:
        if 'dst' in self.addr_sets:
          dst_addr_stmt = ('-m set --match-set %s dst' %
                           self.addr_sets['dst'][0])
      else:
        dst_addr_stmt = '-d %s/%d' % (dst_addr.network_address,
                                      dst_addr.prefixlen)
    return (src_addr_stmt, dst_addr_stmt)

  def _GenerateSetName(self, term_name, suffix):
    if self.af == 'inet6':
      suffix += '-v6'
    if len(term_name) + len(suffix) + 1 > self._SET_MAX_LENGTH:
      set_name_max_lenth = self._SET_MAX_LENGTH - len(suffix) - 1
      term_name = term_name[:set_name_max_lenth]
    return '%s-%s' % (term_name, suffix)


class Ipset(iptables.Iptables):
  """Ipset generator."""
  _PLATFORM = 'ipset'
  _SET_TYPE = 'hash:net'
  SUFFIX = '.ips'
  _TERM = Term
  _MARKER_BEGIN = '# begin:ipset-rules'
  _MARKER_END = '# end:ipset-rules'
  _GOOD_OPTIONS = ['nostate', 'abbreviateterms', 'truncateterms', 'noverbose',
                   'exists']

  # TODO(vklimovs): some not trivial processing is happening inside this
  # __str__, replace with explicit method
  def __str__(self):
    # Actual rendering happens in __str__, so it has to be called
    # before we do set specific part.
    iptables_output = super().__str__()
    output = []
    output.append(self._MARKER_BEGIN)
    for (_, _, _, _, terms) in self.iptables_policies:
      for term in terms:
        output.extend(self._GenerateSetConfig(term))
    output.append(self._MARKER_END)
    output.append(iptables_output)
    return '\n'.join(output)

  def _GenerateSetConfig(self, term):
    """Generates set configuration for supplied term.

    Args:
      term: input term.

    Returns:
      string that is configuration of supplied term.

    """
    output = []
    c_str = 'create'
    a_str = 'add'
    if 'exists' in self.filter_options:
      c_str = c_str + ' -exist'
      a_str = a_str + ' -exist'
    for direction in sorted(term.addr_sets, reverse=True):
      set_name, addr_list = term.addr_sets[direction]
      set_hashsize = 1 << len(addr_list).bit_length()
      set_maxelem = set_hashsize
      output.append('%s %s %s family %s hashsize %i maxelem %i' %
                    (c_str,
                     set_name,
                     self._SET_TYPE,
                     term.af,
                     set_hashsize,
                     set_maxelem))
      for address in addr_list:
        output.append('%s %s %s' % (a_str, set_name, address))
    return output


# capirca/lib/iptables.py

# Copyright 2010 Google 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.
#

"""Iptables generator."""

import datetime
import re
from string import Template  # pylint: disable=g-importing-member

from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import nacaddr


class Term(aclgenerator.Term):
  """Generate Iptables policy terms."""

  # Validate that term does not contain any fields we do not
  # support.  This prevents us from thinking that our output is
  # correct in cases where we've omitted fields from term.
  _PLATFORM = 'iptables'
  _POSTJUMP_FORMAT = None
  _PREJUMP_FORMAT = Template('-A $filter -j $term')
  _TERM_FORMAT = Template('-N $term')
  _COMMENT_FORMAT = Template('-A $term -m comment --comment "$comment"')
  _FILTER_TOP_FORMAT = Template('-A $term')
  _LOG_FORMAT = Template('-j LOG --log-prefix $term')
  _PROTO_TABLE = {
      'icmpv6': '-p ipv6-icmp',
      'icmp': '-p icmp',
      'tcp': '-p tcp',
      'udp': '-p udp',
      'all': '-p all',
      'esp': '-p esp',
      'ah': '-p ah',
      'gre': '-p gre',
      }
  _TCP_FLAGS_TABLE = {
      'syn': 'SYN',
      'ack': 'ACK',
      'fin': 'FIN',
      'rst': 'RST',
      'urg': 'URG',
      'psh': 'PSH',
      'all': 'ALL',
      'none': 'NONE',
      }
  _KNOWN_OPTIONS_MATCHERS = {
      # '! -f' also matches non-fragmented packets.
      'first-fragment': '-m u32 --u32 4&0x3FFF=0x2000',
      'initial': '--syn',
      'tcp-initial': '--syn',
      'sample': '',
      }

  def __init__(self, term, filter_name, trackstate, filter_action, af='inet',
               verbose=True):
    """Setup a new term.

    Args:
      term: A policy.Term object to represent in iptables.
      filter_name: The name of the filter chan to attach the term to.
      trackstate: Specifies if conntrack should be used for new connections.
      filter_action: The default action of the filter.
      af: Which address family ('inet' or 'inet6') to apply the term to.
      verbose: boolean if comments should be printed

    Raises:
      UnsupportedFilterError: Filter is not supported.
    """
    super().__init__(term)
    self._action_table = {
        'accept': '-j ACCEPT',
        'deny': '-j DROP',
        'reject': '-j REJECT --reject-with icmp-host-prohibited',
        'reject-with-tcp-rst': '-j REJECT --reject-with tcp-reset',
        'next': '-j RETURN'
        }
    self.trackstate = trackstate
    self.term = term  # term object
    self.filter = filter_name  # actual name of filter
    self.default_action = filter_action
    self.options = []
    self.af = af
    self.verbose = verbose
    if af == 'inet6':
      self._all_ips = nacaddr.IPv6('::/0')
      self._action_table['reject'] = ('-j REJECT --reject-with '
                                      'icmp6-adm-prohibited')
    else:
      self._all_ips = nacaddr.IPv4('0.0.0.0/0')
      self._action_table['reject'] = ('-j REJECT --reject-with '
                                      'icmp-host-prohibited')

    self.term_name = '%s_%s' % (self.filter[:1], self.term.name)

  def __str__(self):
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if self._PLATFORM not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if self._PLATFORM in self.term.platform_exclude:
        return ''

    ret_str = []

    # Don't render icmpv6 protocol terms under inet, or icmp under inet6
    if ((self.af == 'inet6' and 'icmp' in self.term.protocol) or
        (self.af == 'inet' and 'icmpv6' in self.term.protocol)):
      logging.debug(self.NO_AF_LOG_PROTO.substitute(
          term=self.term.name,
          proto=', '.join(self.term.protocol),
          af=self.af))
      return ''

    # Term verbatim output - this will skip over most normal term
    # creation code by returning early. Warnings provided in policy.py
    if self.term.verbatim:
      for next_verbatim in self.term.verbatim:
        if next_verbatim[0] == self._PLATFORM:
          ret_str.append(str(next_verbatim[1]))
      return '\n'.join(ret_str)

    # Create a new term
    self._SetDefaultAction()
    if self._TERM_FORMAT:
      ret_str.append(self._TERM_FORMAT.substitute(term=self.term_name))

    if self._PREJUMP_FORMAT:
      ret_str.append(self._PREJUMP_FORMAT.substitute(filter=self.filter,
                                                     term=self.term_name))

    if self.verbose:
      if self.term.owner:
        self.term.comment.append('Owner: %s' % self.term.owner)
      # reformat long comments, if needed
      #
      # iptables allows individual comments up to 256 chars.
      # But our generator will limit a single comment line to < 120, using:
      # max = 119 - 27 (static chars in comment command) - [length of term name]
      comment_max_width = 92 - len(self.term_name)
      if comment_max_width < 40:
        comment_max_width = 40
      comments = aclgenerator.WrapWords(self.term.comment, comment_max_width)
      # append comments to output
      if comments and comments[0]:
        for line in comments:
          if not line:
            continue  # iptables-restore does not like 0-length comments.
          # term comments
          # Strip out quotes as iptables cant have nested quotes
          ret_str.append(self._COMMENT_FORMAT.substitute(
              filter=self.filter,
              term=self.term_name,
              comment=str(line).replace('\"', '')))

    # Unsupported configuration; in the case of 'accept' or 'next', we
    # skip the rule.  In other cases, we blow up (raise an exception)
    # to ensure that this is not considered valid configuration.
    if self.term.source_prefix or self.term.destination_prefix:
      if str(self.term.action[0]) not in set(['accept', 'next']):
        raise UnsupportedFilterError('%s %s %s %s %s %s %s %s' % (
            '\nTerm', self.term.name, 'has action', str(self.term.action[0]),
            'with source_prefix or destination_prefix,',
            ' which is unsupported in', self._PLATFORM, 'iptables output.'))
      return ('# skipped %s due to source or destination prefix rule' %
              self.term.name)

    # protocol
    if self.term.protocol:
      protocol = self.term.protocol
    else:
      protocol = ['all']
    if 'hopopt' in protocol and self.af == 'inet':
      logging.warning('Term %s is using hopopt in IPv4 context.',
                      self.term_name)
      return ''

    (term_saddr, exclude_saddr,
     term_daddr, exclude_daddr) = self._CalculateAddresses(
         self.term.source_address, self.term.source_address_exclude,
         self.term.destination_address, self.term.destination_address_exclude)
    if not term_saddr:
      logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                   direction='source',
                                                   af=self.af))
      return ''
    if not term_daddr:
      logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                   direction='destination',
                                                   af=self.af))
      return ''

    # ports
    source_port = []
    destination_port = []
    if self.term.source_port:
      source_port = self.term.source_port
    if self.term.destination_port:
      destination_port = self.term.destination_port

    # icmp-types
    icmp_types = ['']
    if self.term.icmp_type:
      icmp_types = self.NormalizeIcmpTypes(self.term.icmp_type, protocol,
                                           self.af)

    source_interface = ''
    if self.term.source_interface:
      source_interface = self.term.source_interface

    destination_interface = ''
    if self.term.destination_interface:
      destination_interface = self.term.destination_interface

    log_hits = False
    if self.term.logging:
      # Iptables sends logs to hosts configured syslog
      log_hits = True

    # options
    tcp_flags = []
    tcp_track_options = []
    for next_opt in [str(x) for x in self.term.option]:
      #
      # Sanity checking and high-ports are added as appropriate in
      # pre-processing that is done in __str__ within class Iptables.
      # Option established will add destination port high-ports if protocol
      # contains only tcp, udp or both.  This is done earlier in class Iptables.
      #
      if ((next_opt.find('established') == 0 or
           next_opt.find('tcp-established') == 0)
          and 'ESTABLISHED' not in [x.strip() for x in self.options]):
        if next_opt.find('tcp-established') == 0 and protocol != ['tcp']:
          raise TcpEstablishedError('%s %s %s' % (
              '\noption tcp-established can only be applied for proto tcp.',
              '\nError in term:', self.term.name))

        if self.trackstate:
          # Use nf_conntrack to track state -- works with any proto
          self.options.append('-m state --state ESTABLISHED,RELATED')
        elif protocol == ['tcp']:
          # Simple established-only rule for TCP: Must have ACK field
          # (SYN/ACK or subsequent ACK), or RST and no other flags.
          tcp_track_options = [(['ACK'], ['ACK']),
                               (['SYN', 'FIN', 'ACK', 'RST'], ['RST'])]

      # Iterate through flags table, and create list of tcp-flags to append
      for next_flag in self._TCP_FLAGS_TABLE:
        if next_opt.find(next_flag) == 0:
          tcp_flags.append(self._TCP_FLAGS_TABLE.get(next_flag))
      if next_opt in self._KNOWN_OPTIONS_MATCHERS:
        self.options.append(self._KNOWN_OPTIONS_MATCHERS[next_opt])
    if self.term.packet_length:
      # Policy format is "#-#", but iptables format is "#:#"
      self.options.append('-m length --length %s' %
                          self.term.packet_length.replace('-', ':'))
    if self.term.fragment_offset:
      self.options.append('-m u32 --u32 4&0x1FFF=%s' %
                          self.term.fragment_offset.replace('-', ':'))
    icmp_code = ['']
    if self.term.icmp_code:
      icmp_code = self.term.icmp_code

    for saddr in exclude_saddr:
      ret_str.extend(self._FormatPart(
          '', saddr, '', '', '', '', '', '', '', '', '', '', '',
          self._action_table.get('next')))
    for daddr in exclude_daddr:
      ret_str.extend(self._FormatPart(
          '', '', '', daddr, '', '', '', '', '', '', '', '', '',
          self._action_table.get('next')))

    for saddr in term_saddr:
      for daddr in term_daddr:
        for icmp in icmp_types:
          for code in icmp_code:
            for proto in protocol:
              for tcp_matcher in tcp_track_options or (([], []),):
                ret_str.extend(self._FormatPart(
                    str(proto),
                    saddr,
                    source_port,
                    daddr,
                    destination_port,
                    self.options,
                    tcp_flags,
                    icmp,
                    code,
                    tcp_matcher,
                    source_interface,
                    destination_interface,
                    log_hits,
                    self._action_table.get(str(self.term.action[0]))
                    ))

    if self._POSTJUMP_FORMAT:
      ret_str.append(self._POSTJUMP_FORMAT.substitute(filter=self.filter,
                                                      term=self.term_name))

    return '\n'.join(str(v) for v in ret_str if v)

  def _CalculateAddresses(self, term_saddr, exclude_saddr,
                          term_daddr, exclude_daddr):
    """Calculate source and destination address list for a term.

    Args:
      term_saddr: source address list of the term
      exclude_saddr: source address exclude list of the term
      term_daddr: destination address list of the term
      exclude_daddr: destination address exclude list of the term

    Returns:
      tuple containing source address list, source exclude address list,
      destination address list, destination exclude address list in
      that order

    """
    # source address
    term_saddr_excluded = []
    if not term_saddr:
      term_saddr = [self._all_ips]
    if exclude_saddr:
      term_saddr_excluded.extend(nacaddr.ExcludeAddrs(term_saddr,
                                                      exclude_saddr))

    # destination address
    term_daddr_excluded = []
    if not term_daddr:
      term_daddr = [self._all_ips]
    if exclude_daddr:
      term_daddr_excluded.extend(nacaddr.ExcludeAddrs(term_daddr,
                                                      exclude_daddr))

    # Just to be safe, always have a result of at least 1 to avoid * by zero
    # returning incorrect results (10src*10dst=100, but 10src*0dst=0, not 10)
    bailout_count = len(exclude_saddr) + len(exclude_daddr) + (
        (len(self.term.source_address) or 1) *
        (len(self.term.destination_address) or 1))
    exclude_count = ((len(term_saddr_excluded) or 1) *
                     (len(term_daddr_excluded) or 1))

    # Use bailout jumps for excluded addresses if it results in fewer output
    # lines than nacaddr.ExcludeAddrs() method.
    if exclude_count < bailout_count:
      exclude_saddr = []
      exclude_daddr = []
      if term_saddr_excluded:
        term_saddr = term_saddr_excluded
      if term_daddr_excluded:
        term_daddr = term_daddr_excluded

    # With many sources and destinations, iptables needs to generate the
    # cartesian product of sources and destinations.  If there are no
    # exclude rules, this can instead be written as exclude [0/0 -
    # srcs], exclude [0/0 - dsts].
    v4_src_count = len([x for x in term_saddr if x.version == 4])
    v4_dst_count = len([x for x in term_daddr if x.version == 4])
    v6_src_count = len([x for x in term_saddr if x.version == 6])
    v6_dst_count = len([x for x in term_daddr if x.version == 6])
    num_pairs = v4_src_count * v4_dst_count + v6_src_count * v6_dst_count
    if num_pairs > 100:
      new_exclude_source = nacaddr.ExcludeAddrs([self._all_ips], term_saddr)
      new_exclude_dest = nacaddr.ExcludeAddrs([self._all_ips], term_daddr)
      # Invert the shortest list that does not already have exclude addresses
      if len(new_exclude_source) < len(new_exclude_dest) and not exclude_saddr:
        if len(new_exclude_source) + len(term_daddr) < num_pairs:
          exclude_saddr = new_exclude_source
          term_saddr = [self._all_ips]
      elif not exclude_daddr:
        if len(new_exclude_dest) + len(term_saddr) < num_pairs:
          exclude_daddr = new_exclude_dest
          term_daddr = [self._all_ips]
    term_saddr = [x for x in term_saddr
                  if x.version == self.AF_MAP[self.af]]
    exclude_saddr = [x for x in exclude_saddr
                     if x.version == self.AF_MAP[self.af]]
    term_daddr = [x for x in term_daddr
                  if x.version == self.AF_MAP[self.af]]
    exclude_daddr = [x for x in exclude_daddr
                     if x.version == self.AF_MAP[self.af]]
    return (term_saddr, exclude_saddr, term_daddr, exclude_daddr)

  def _FormatPart(self, protocol, saddr, sport, daddr, dport, options,
                  tcp_flags, icmp_type, code, track_flags, sint, dint, log_hits,
                  action):
    """Compose one iteration of the term parts into a string.

    Args:
      protocol: The network protocol
      saddr: Source IP address
      sport: Source port numbers
      daddr: Destination IP address
      dport: Destination port numbers
      options: Optional arguments to append to our rule
      tcp_flags: Which tcp_flag arguments, if any, should be appended
      icmp_type: What icmp protocol to allow, if any
      code: ICMP code allowed, if any
      track_flags: A tuple of ([check-flags], [set-flags]) arguments to tcp-flag
      sint: Optional source interface
      dint: Optional destination interface
      log_hits: Boolean, to log matches or not
      action: What should happen if this rule matches
    Returns:
      rval:  A single iptables argument line
    """
    src, dst = self._GenerateAddressStatement(saddr, daddr)

    filter_top = self._FILTER_TOP_FORMAT.substitute(filter=self.filter,
                                                    term=self.term_name)

    source_int = ''
    if sint:
      source_int = '-i %s' % sint

    destination_int = ''
    if dint:
      destination_int = '-o %s' % dint

    log_jump = ''
    if log_hits:
      log_jump = self._LOG_FORMAT.substitute(term=self.term.name)
      if self.term.log_limit:
        log_jump = '-m --limit {}/{} {}'.format(
            self.term.log_limit[0], self.term.log_limit[1], log_jump)

    if not options:
      options = []

    proto = self._PROTO_TABLE.get(str(protocol))
    # Don't drop protocol if we don't recognize it
    if protocol and not proto:
      proto = '-p %s' % str(protocol)

    # TODO(vklimovs): generalize to all v6 special cases
    # Use u32 module as named modules are not available
    # everywhere.
    if protocol == 'hopopt':
      proto = ''
      # Select 4 bytes at offset 0x3, mask out all but
      # last byte. That produces byte at position 7,
      # Next Header. Compare to 0x0, Hop By Hop
      options.append('-m u32 --u32 "0x3&0xff=0x0"')
    if protocol == 'fragment':
      proto = ''
      # Ditto, but compare to 0x2c, 44, Fragment
      options.append('-m u32 --u32 "0x3&0xff=0x2c"')

    # set conntrack state to NEW, unless policy requested "nostate"
    # for icmpv6 use trackstate only for type 128 and 139, as this is the
    # only supported types in nf_conntrack_proto_icmpv6
    if self.trackstate and (
        protocol != 'icmpv6' or icmp_type in [128, 139]
    ):
      already_stateful = False
      # we will add new stateful arguments only if none already exist, such
      # as from "option:: established"
      for option in options:
        if 'state' in option:
          already_stateful = True
      if not already_stateful:
        if 'ACCEPT' in action:
          # We have to permit established/related since a policy may not
          # have an existing blank permit for established/related, which
          # may be more efficient, but slightly less secure.
          options.append('-m state --state NEW,ESTABLISHED,RELATED')

    if tcp_flags or (track_flags and track_flags[0]):
      check_fields = ','.join(sorted(set(tcp_flags + track_flags[0])))
      set_fields = ','.join(sorted(set(tcp_flags + track_flags[1])))
      flags = '--tcp-flags %s %s' % (check_fields, set_fields)
    else:
      flags = ''

    icmp_type = str(icmp_type)
    if not icmp_type:
      icmp = ''
    elif str(protocol) == 'icmpv6':
      icmp = '-m icmp6 --icmpv6-type %s' % icmp_type
    else:
      icmp = '--icmp-type %s' % icmp_type
    if code:
      icmp += r'/%d' % code

    # format tcp and udp ports
    sports = dports = ['']
    if sport:
      sports = self._GeneratePortStatement(sport, source=True)
    if dport:
      dports = self._GeneratePortStatement(dport, dest=True)

    ret_lines = []
    for sport in sports:
      for dport in dports:
        rval = [filter_top]
        if re.search('multiport', sport) and not re.search('multiport', dport):
          # Due to bug in iptables, use of multiport module before a single
          # port specification will result in multiport trying to consume it.
          # this is a little hack to ensure single ports are listed before
          # any multiport specification.
          dport, sport = sport, dport
        if str(protocol) == 'icmpv6':
          # Due to a bug in ip6tables, iptables-save returns icmpv6 matches in
          # order (address spec) (icmpv6 spec). Fake this using options
          # datastructure.
          options.extend((proto, icmp))
          proto = ''
          icmp = ''
        if (str(self.af) == 'inet6' and
            str(protocol) == 'all' and
            'REJECT' in str(action)):
          # Due to a bug in ip6tables, when -p all and -j REJECT, proto
          # is being eaten
          proto = ''
        for value in (proto, flags, sport, dport, icmp, src, dst,
                      ' '.join(options), source_int, destination_int):
          if value:
            rval.append(str(value))
        if log_jump:
          # -j LOG
          ret_lines.append(' '.join(rval+[log_jump]))
        # -j ACTION
        ret_lines.append(' '.join(rval+[action]))
    return ret_lines

  def _GenerateAddressStatement(self, saddr, daddr):
    """Return the address section of an individual iptables rule.

    Args:
      saddr: source address of the rule
      daddr: destination address of the rule

    Returns:
      tuple containing source and destination address statement, in
      that order

    """
    src = ''
    dst = ''
    if not saddr or saddr == self._all_ips:
      src = ''
    else:
      src = '-s %s/%d' % (saddr.network_address, saddr.prefixlen)
    if not daddr or daddr == self._all_ips:
      dst = ''
    else:
      dst = '-d %s/%d' % (daddr.network_address, daddr.prefixlen)
    return (src, dst)

  def _GeneratePortStatement(self, ports, source=False, dest=False):
    """Return the 'port' section of an individual iptables rule.

    Args:
      ports: list of ports or port ranges (pairs)
      source: (bool) generate a source port rule
      dest: (bool) generate a dest port rule

    Returns:
      list holding the 'port' sections of an iptables rule.

    Raises:
      BadPortsError: if too many ports are passed in, or if both 'source'
                        and 'dest' are true.
      NotImplementedError: if both 'source' and 'dest' are true.
    """
    if not ports:
      return ''

    direction = ''  # default: no direction / '--port'.  As yet, unused.
    if source and dest:
      raise BadPortsError('_GeneratePortStatement called ambiguously.')
    elif source:
      direction = 's'  # source port / '--sport'
    elif dest:
      direction = 'd'  # dest port / '--dport'
    else:
      raise NotImplementedError('--port support not yet implemented.')

    # Normalize ports and get accurate port count.
    # iptables multiport module limits to 15, but we use 14 to ensure a range
    # doesn't tip us over the limit
    max_ports = 14
    norm_ports = []
    portstrings = []
    count = 0
    for port in ports:
      if port[0] == port[1]:
        norm_ports.append(str(port[0]))
        count += 1
      else:
        norm_ports.append('%d:%d' % (port[0], port[1]))
        count += 2
      if count >= max_ports:
        count = 0
        portstrings.append('-m multiport --%sports %s' % (direction,
                                                          ','.join(norm_ports)))
        norm_ports = []
    if norm_ports:
      if len(norm_ports) == 1:
        portstrings.append('--%sport %s' % (direction, norm_ports[0]))
      else:
        portstrings.append('-m multiport --%sports %s' %
                           (direction, ','.join(norm_ports)))
    return portstrings

  def _SetDefaultAction(self):
    """If term does not specify action, use filter default action."""
    if not self.term.action:
      self.term.action[0].value = self.default_action


class Iptables(aclgenerator.ACLGenerator):
  """Generates filters and terms from provided policy object."""

  _PLATFORM = 'iptables'
  _DEFAULT_PROTOCOL = 'all'
  SUFFIX = ''
  _RENDER_PREFIX = None
  _RENDER_SUFFIX = None
  _DEFAULTACTION_FORMAT = '-P %s %s'
  _DEFAULTACTION_FORMAT_CUSTOM_CHAIN = '-N %s'
  _DEFAULT_ACTION = 'DROP'
  _TERM = Term
  _TERM_MAX_LENGTH = 24
  _GOOD_FILTERS = ['INPUT', 'OUTPUT', 'FORWARD']
  _GOOD_OPTIONS = ['nostate', 'abbreviateterms', 'truncateterms', 'noverbose']

  def __init__(self, pol, exp_info):
    self.iptables_policies = []
    super().__init__(pol, exp_info)

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'counter',
                         'destination_interface',
                         'destination_prefix',
                         'fragment_offset',
                         'icmp_code',
                         'logging',
                         'log_limit',
                         'owner',
                         'packet_length',
                         'routing_instance',
                         'source_interface',
                         'source_prefix'}

    supported_sub_tokens.update(
        {'option': {'established',
                    'first-fragment',
                    'initial',
                    'sample',
                    'tcp-established',
                    'tcp-initial',
                    'syn',
                    'ack',
                    'fin',
                    'rst',
                    'urg',
                    'psh',
                    'all',
                    'none'},
        })
    return supported_tokens, supported_sub_tokens

  def _WarnIfCustomTarget(self, target):
    """Emit a warning if a policy's default target is not a built-in chain."""
    if target not in self._GOOD_FILTERS:
      logging.warning('Filter is generating a non-standard chain that will not '
                      'apply to traffic unless linked from INPUT, OUTPUT or '
                      'FORWARD filters. New chain name is: %s', target)

  def _TranslatePolicy(self, pol, exp_info):
    """Translate a policy from objects into strings."""
    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    default_action = None
    good_default_actions = ['ACCEPT', 'DROP']
    good_afs = ['inet', 'inet6']
    all_protocols_stateful = True
    self.verbose = True

    for header, terms in pol.filters:
      filter_type = None
      if self._PLATFORM not in header.platforms:
        continue

      self.filter_options = header.FilterOptions(self._PLATFORM)[1:]
      filter_name = header.FilterName(self._PLATFORM)

      self._WarnIfCustomTarget(filter_name)

      # ensure all options after the filter name are expected
      for opt in self.filter_options:
        if opt not in good_default_actions + good_afs + self._GOOD_OPTIONS:
          raise UnsupportedTargetOptionError('%s %s %s %s' % (
              '\nUnsupported option found in', self._PLATFORM,
              'target definition:', opt))

      # disable stateful?
      if 'nostate' in self.filter_options:
        all_protocols_stateful = False
      if 'noverbose' in self.filter_options:
        self.verbose = False

      # Check for matching af
      for address_family in good_afs:
        if address_family in self.filter_options:
          # should not specify more than one AF in options
          if filter_type is not None:
            raise UnsupportedFilterError('%s %s %s %s' % (
                '\nMay only specify one of', good_afs, 'in filter options:',
                self.filter_options))
          filter_type = address_family
      if filter_type is None:
        filter_type = 'inet'

      if self._PLATFORM == 'iptables' and filter_name == 'FORWARD':
        default_action = 'DROP'

      # does this policy override the default filter actions?
      for next_target in header.target:
        if next_target.platform == self._PLATFORM:
          if len(next_target.options) > 1:
            for arg in next_target.options:
              if arg in good_default_actions:
                default_action = arg
      if default_action and default_action not in good_default_actions:
        raise UnsupportedDefaultActionError('%s %s %s %s %s' % (
            '\nOnly', ', '.join(good_default_actions),
            'default filter action allowed;', default_action, 'used.'))

      # add the terms
      new_terms = []
      term_names = set()
      for term in terms:
        term.name = self.FixTermLength(term.name,
                                       'abbreviateterms' in self.filter_options,
                                       'truncateterms' in self.filter_options)
        if term.name in term_names:
          raise aclgenerator.DuplicateTermError(
              'You have a duplicate term: %s' % term.name)
        term_names.add(term.name)
        if not term.logging and term.log_limit:
          raise LimitButNoLogError(
              'Term %s: Cannoy use log-limit without logging' % term.name)

        term = self.FixHighPorts(term, af=filter_type,
                                 all_protocols_stateful=all_protocols_stateful)
        if not term:
          continue

        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue

        new_terms.append(self._TERM(term, filter_name, all_protocols_stateful,
                                    default_action, filter_type, self.verbose))

      self.iptables_policies.append((header, filter_name, filter_type,
                                     default_action, new_terms))

  def SetTarget(self, target, action=None):
    """Sets policy's target and default action.

    Args:
      target: (string) target name
      action: (string) default action, only valid if target is a built-in chain
    """
    # there is only one item in iptables_policies
    pol = list(self.iptables_policies[0])
    pol[1] = target
    self._WarnIfCustomTarget(target)
    if action:
      pol[3] = action
    self.iptables_policies[0] = tuple(pol)

  def __str__(self):
    target = []
    pretty_platform = '%s%s' % (self._PLATFORM[0].upper(), self._PLATFORM[1:])

    if self._RENDER_PREFIX:
      target.append(self._RENDER_PREFIX)

    for (header, filter_name, filter_type, default_action, terms
        ) in self.iptables_policies:
      # Add comments for this filter
      target.append('# %s %s Policy' % (pretty_platform,
                                        header.FilterName(self._PLATFORM)))

      # reformat long text comments, if needed
      comments = aclgenerator.WrapWords(header.comment, 70)
      if comments and comments[0]:
        for line in comments:
          target.append('# %s' % line)
        target.append('#')
      # add the p4 tags
      target.extend(aclgenerator.AddRepositoryTags('# '))
      target.append('# ' + filter_type)

      if filter_name in self._GOOD_FILTERS:
        if default_action:
          target.append(self._DEFAULTACTION_FORMAT % (filter_name,
                                                      default_action))
        elif self._PLATFORM == 'speedway':
          # always specify the default filter states for speedway,
          # if default action policy not specified for iptables, do nothing.
          target.append(
              self._DEFAULTACTION_FORMAT % (filter_name, self._DEFAULT_ACTION))
      else:
        # Custom chains have no concept of default policy.
        target.append(self._DEFAULTACTION_FORMAT_CUSTOM_CHAIN % filter_name)
      # add the terms
      for term in terms:
        term_str = str(term)
        if term_str:
          target.append(term_str)

    if self._RENDER_SUFFIX:
      target.append(self._RENDER_SUFFIX)

    target.append('')
    return '\n'.join(target)


class Error(Exception):
  """Base error class."""


class BadPortsError(Error):
  """Too many ports for a single iptables statement."""


class UnsupportedFilterError(Error):
  """Raised when we see an inappropriate filter."""


class NoIptablesPolicyError(Error):
  """Raised when a policy is received that doesn't support iptables."""


class TcpEstablishedError(Error):
  """Raised when a term has tcp-established option but not proto tcp only."""


class EstablishedError(Error):
  """Raised when a term has established option with inappropriate protocol."""


class UnsupportedDefaultActionError(Error):
  """Raised when a filter has an impermissible default action specified."""


class UnsupportedTargetOptionError(Error):
  """Raised when a filter has an impermissible default action specified."""


class LimitButNoLogError(Error):
  """Raised when log-limit is used by logging is not."""


# capirca/lib/juniper.py

# Copyright 2011 Google 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.
#

"""Juniper JCL generator."""

import datetime
from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import summarizer
import six


# generic error class
class Error(Exception):
  pass


class JuniperTermPortProtocolError(Error):
  pass


class TcpEstablishedWithNonTcpError(Error):
  pass


class JuniperDuplicateTermError(Error):
  pass


class UnsupportedFilterError(Error):
  pass


class PrecedenceError(Error):
  pass


class JuniperIndentationError(Error):
  pass


class JuniperNextIpError(Error):
  pass


class JuniperMultipleTerminatingActionError(Error):
  pass


class JuniperFragmentInV6Error(Error):
  pass


class Config:
  """Config allows a configuration to be assembled easily.

  Configurations are automatically indented following Juniper's style.
  A textual representation of the config can be extracted with str().

  Attributes:
    indent: The number of leading spaces on the current line.
    tabstop: The number of spaces to indent for a new level.
    lines: the text lines of the configuration.
  """

  def __init__(self, indent=0, tabstop=4):
    self.indent = indent
    self._initial_indent = indent
    self.tabstop = tabstop
    self.lines = []

  def __str__(self):
    if self.indent != self._initial_indent:
      raise JuniperIndentationError(
          'Expected indent %d but got %d' % (self._initial_indent, self.indent))
    return '\n'.join(self.lines)

  def Append(self, line, verbatim=False):
    """Append one line to the configuration.

    Args:
      line: The string to append to the config.
      verbatim: append line without adjusting indentation. Default False.
    Raises:
      JuniperIndentationError: If the indentation would be further left
        than the initial indent.  e.g. too many close braces.
    """
    if verbatim:
      self.lines.append(line)
      return

    if line.endswith('}'):
      self.indent -= self.tabstop
      if self.indent < self._initial_indent:
        raise JuniperIndentationError('Too many close braces.')
    spaces = ' ' * self.indent
    self.lines.append(spaces + line.strip())
    if not line.find('/*') >= 0 and line.find('*/') >= 0:
      self.indent -= 1
      if self.indent < self._initial_indent:
        raise JuniperIndentationError('Too many close comments.')
    if not line.find('*/') >= 0 and line.find('/*') >= 0:
      self.indent += 1
    if line.endswith(' {'):
      self.indent += self.tabstop


class Term(aclgenerator.Term):
  """Representation of an individual Juniper term.

    This is mostly useful for the __str__() method.
  Attributes:
    term: The term object from policy.
    term_type: String indicating type of term, inet, inet6 icmp etc.
    enable_dsmo: Boolean to enable dsmo.
    noverbose: Boolean to disable verbosity.
  """
  _PLATFORM = 'juniper'
  _DEFAULT_INDENT = 12
  ACTIONS = {'accept': 'accept',
             'deny': 'discard',
             'reject': 'reject',
             'next': 'next term',
             'reject-with-tcp-rst': 'reject tcp-reset',
             'encapsulate': 'encapsulate'}

  # the following lookup table is used to map between the various types of
  # filters the juniper generator can render.  As new differences are
  # encountered, they should be added to this table.  Accessing members
  # of this table looks like:
  #  self._TERM_TYPE('inet').get('saddr') -> 'source-address'
  #
  # it's critical that the members of each filter type be the same, that is
  # to say that if _TERM_TYPE.get('inet').get('foo') returns something,
  # _TERM_TYPE.get('inet6').get('foo') must return the inet6 equivalent.
  _TERM_TYPE = {'inet': {'addr': 'address',
                         'saddr': 'source-address',
                         'daddr': 'destination-address',
                         'protocol': 'protocol',
                         'protocol-except': 'protocol-except',
                         'tcp-est': 'tcp-established'},
                'inet6': {'addr': 'address',
                          'saddr': 'source-address',
                          'daddr': 'destination-address',
                          'protocol': 'next-header',
                          'protocol-except': 'next-header-except',
                          'tcp-est': 'tcp-established'},
                'bridge': {'addr': 'ip-address',
                           'saddr': 'ip-source-address',
                           'daddr': 'ip-destination-address',
                           'protocol': 'ip-protocol',
                           'protocol-except': 'ip-protocol-except',
                           'tcp-est': 'tcp-flags "(ack|rst)"'}
              }

  def __init__(self, term, term_type, enable_dsmo, noverbose):
    super().__init__(term)
    self.term = term
    self.term_type = term_type
    self.enable_dsmo = enable_dsmo
    self.noverbose = noverbose

    if self._PLATFORM != 'msmpc':
      if term_type not in self._TERM_TYPE:
        raise ValueError('Unknown Filter Type: %s' % term_type)
      if 'hopopt' in self.term.protocol:
        loc = self.term.protocol.index('hopopt')
        self.term.protocol[loc] = 'hop-by-hop'

      # some options need to modify the actions
      self.extra_actions = []

  # TODO(pmoody): get rid of all of the default string concatenation here.
  #  eg, indent(8) + 'foo;' -> '%s%s;' % (indent(8), 'foo'). pyglint likes this
  #  more.
  def __str__(self):
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if self._PLATFORM not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if self._PLATFORM in self.term.platform_exclude:
        return ''

    config = Config(indent=self._DEFAULT_INDENT)
    from_str = []
    # Don't render icmpv6 protocol terms under inet, or icmp under inet6
    if ((self.term_type == 'inet6' and 'icmp' in self.term.protocol) or
        (self.term_type == 'inet' and ('icmpv6' in self.term.protocol or
                                       'icmp6' in self.term.protocol))):
      logging.debug(self.NO_AF_LOG_PROTO.substitute(
          term=self.term.name,
          proto=', '.join(self.term.protocol),
          af=self.term_type))
      return ''

    # comment
    # this deals just fine with multi line comments, but we could probably
    # output them a little cleaner; do things like make sure the
    # len(output) < 80, etc. Note, if 'noverbose' is set for the filter, skip
    # all comment processing.
    if self.term.owner and not self.noverbose:
      self.term.comment.append('Owner: %s' % self.term.owner)
    if self.term.comment and not self.noverbose:
      config.Append('/*')
      for comment in self.term.comment:
        for line in comment.split('\n'):
          config.Append('** ' + line)
      config.Append('*/')

    # Term verbatim output - this will skip over normal term creation
    # code.  Warning generated from policy.py if appropriate.
    if self.term.verbatim:
      for next_term in self.term.verbatim:
        if next_term[0] == self._PLATFORM:
          config.Append(str(next_term[1]), verbatim=True)
      return str(config)

    # Helper for per-address-family keywords.
    family_keywords = self._TERM_TYPE.get(self.term_type)

    # option
    # this is going to be a little ugly b/c there are a few little messed
    # up options we can deal with.
    if self.term.option:
      for opt in [str(x) for x in self.term.option]:
        # there should be a better way to search the array of protocols
        if opt.startswith('sample'):
          self.extra_actions.append('sample')

        # only append tcp-established for option established when
        # tcp is the only protocol, otherwise other protos break on juniper
        elif opt.startswith('established'):
          if self.term.protocol == ['tcp']:
            if 'tcp-established;' not in from_str:
              from_str.append(family_keywords['tcp-est'] + ';')

        # if tcp-established specified, but more than just tcp is included
        # in the protocols, raise an error
        elif opt.startswith('tcp-established'):
          flag = family_keywords['tcp-est'] + ';'
          if self.term.protocol == ['tcp']:
            if flag not in from_str:
              from_str.append(flag)
          else:
            raise TcpEstablishedWithNonTcpError(
                'tcp-established can only be used with tcp protocol in term %s'
                % self.term.name)
        elif opt.startswith('rst'):
          from_str.append('tcp-flags "rst";')
        elif opt.startswith('initial') and 'tcp' in self.term.protocol:
          from_str.append('tcp-initial;')
        elif opt.startswith('first-fragment'):
          from_str.append('first-fragment;')

        # we don't have a special way of dealing with this, so we output it and
        # hope the user knows what they're doing.
        else:
          from_str.append('%s;' % opt)

    # if the term is inactive we have to set the prefix
    if self.term.inactive:
      term_prefix = 'inactive:'
    else:
      term_prefix = ''

    # term name
    config.Append('%s term %s {' % (term_prefix, self.term.name))

    # The "filter" keyword is not compatible with from or then
    if self.term.filter_term:
      config.Append('filter %s;' % self.term.filter_term)
      config.Append('}')  # end term accept-foo-to-bar { ... }
      return str(config)

    # a default action term doesn't have any from { clause
    has_match_criteria = (self.term.address or
                          self.term.dscp_except or
                          self.term.dscp_match or
                          self.term.destination_address or
                          self.term.destination_port or
                          self.term.destination_prefix or
                          self.term.destination_prefix_except or
                          self.term.encapsulate or
                          self.term.ether_type or
                          self.term.flexible_match_range or
                          self.term.forwarding_class or
                          self.term.forwarding_class_except or
                          self.term.fragment_offset or
                          self.term.hop_limit or
                          self.term.next_ip or
                          self.term.port or
                          self.term.precedence or
                          self.term.protocol or
                          self.term.protocol_except or
                          self.term.source_address or
                          self.term.source_port or
                          self.term.source_prefix or
                          self.term.source_prefix_except or
                          self.term.traffic_type or
                          self.term.ttl)

    if has_match_criteria:
      config.Append('from {')

      term_af = self.AF_MAP.get(self.term_type)

      # address
      address = self.term.GetAddressOfVersion('address', term_af)
      if self.enable_dsmo:
        address = summarizer.Summarize(address)

      if address:
        config.Append('%s {' % family_keywords['addr'])
        for addr in address:
          for comment in self._Comment(addr):
            config.Append('%s' % comment)
          if self.enable_dsmo:
            config.Append('%s/%s;' % summarizer.ToDottedQuad(addr, nondsm=True))
          else:
            config.Append('%s;' % addr)
        config.Append('}')
      elif self.term.address:
        logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                     af=self.term_type))
        return ''

      # source address

      src_addr = self.term.GetAddressOfVersion('source_address', term_af)
      src_addr_ex = self.term.GetAddressOfVersion('source_address_exclude',
                                                  term_af)
      if self.enable_dsmo:
        src_addr = summarizer.Summarize(src_addr)
        src_addr_ex = summarizer.Summarize(src_addr_ex)
      else:
        src_addr, src_addr_ex = self._MinimizePrefixes(src_addr, src_addr_ex)

      if src_addr:
        config.Append('%s {' % family_keywords['saddr'])
        for addr in src_addr:
          for comment in self._Comment(addr):
            config.Append('%s' % comment)
          if self.enable_dsmo:
            config.Append('%s/%s;' % summarizer.ToDottedQuad(addr, nondsm=True))
          else:
            config.Append('%s;' % addr)
        for addr in src_addr_ex:
          for comment in self._Comment(addr, exclude=True):
            config.Append('%s' % comment)
          if self.enable_dsmo:
            config.Append('%s/%s except;' %
                          summarizer.ToDottedQuad(addr, nondsm=True))
          else:
            config.Append('%s except;' % addr)
        config.Append('}')
      elif self.term.source_address:
        logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                     direction='source',
                                                     af=self.term_type))
        return ''

      # destination address
      dst_addr = self.term.GetAddressOfVersion('destination_address', term_af)
      dst_addr_ex = self.term.GetAddressOfVersion('destination_address_exclude',
                                                  term_af)
      if self.enable_dsmo:
        dst_addr = summarizer.Summarize(dst_addr)
        dst_addr_ex = summarizer.Summarize(dst_addr_ex)
      else:
        dst_addr, dst_addr_ex = self._MinimizePrefixes(dst_addr, dst_addr_ex)

      if dst_addr:
        config.Append('%s {' % family_keywords['daddr'])
        for addr in dst_addr:
          for comment in self._Comment(addr):
            config.Append('%s' % comment)
          if self.enable_dsmo:
            config.Append('%s/%s;' % summarizer.ToDottedQuad(addr,
                                                             nondsm=True))
          else:
            config.Append('%s;' % addr)
        for addr in dst_addr_ex:
          for comment in self._Comment(addr, exclude=True):
            config.Append('%s' % comment)
          if self.enable_dsmo:
            config.Append('%s/%s except;' %
                          summarizer.ToDottedQuad(addr, nondsm=True))
          else:
            config.Append('%s except;' % addr)
        config.Append('}')
      elif self.term.destination_address:
        logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                     direction='destination',
                                                     af=self.term_type))
        return ''

      # forwarding-class
      if self.term.forwarding_class:
        config.Append('forwarding-class %s' % self._Group(
            self.term.forwarding_class, lc=False))

      # forwarding-class-except
      if self.term.forwarding_class_except:
        config.Append('forwarding-class-except %s' % self._Group(
            self.term.forwarding_class_except, lc=False))

      # source prefix <except> list
      if self.term.source_prefix or self.term.source_prefix_except:
        config.Append('source-prefix-list {')
        for pfx in self.term.source_prefix:
          config.Append(pfx + ';')
        for epfx in self.term.source_prefix_except:
          config.Append(epfx + ' except;')
        config.Append('}')

      # destination prefix <except> list
      if self.term.destination_prefix or self.term.destination_prefix_except:
        config.Append('destination-prefix-list {')
        for pfx in self.term.destination_prefix:
          config.Append(pfx + ';')
        for epfx in self.term.destination_prefix_except:
          config.Append(epfx + ' except;')
        config.Append('}')

      # Only generate ttl if inet, inet6 uses hop-limit instead.
      if self.term.ttl and self.term_type == 'inet':
        config.Append('ttl %s;' % self.term.ttl)

      # protocol
      if self.term.protocol:
        # both are supported on JunOS, but only icmp6 is supported
        # on SRX loopback stateless filter
        config.Append(family_keywords['protocol'] +
                      ' ' + self._Group(self.term.protocol))

      # protocol
      if self.term.protocol_except:
        # same as above
        config.Append(family_keywords['protocol-except'] + ' ' + self._Group(self.term.protocol_except))

      # port
      if self.term.port:
        config.Append('port %s' % self._Group(self.term.port))

      # source port
      if self.term.source_port:
        config.Append('source-port %s' % self._Group(self.term.source_port))

      # destination port
      if self.term.destination_port:
        config.Append('destination-port %s' %
                      self._Group(self.term.destination_port))

      # append any options beloging in the from {} section
      for next_str in from_str:
        config.Append(next_str)

      # packet length
      if self.term.packet_length:
        config.Append('packet-length %s;' % self.term.packet_length)

      # fragment offset
      if self.term.fragment_offset:
        config.Append('fragment-offset %s;' % self.term.fragment_offset)

      # icmp-types
      icmp_types = ['']
      if self.term.icmp_type:
        icmp_types = self.NormalizeIcmpTypes(self.term.icmp_type,
                                             self.term.protocol, self.term_type)
      if icmp_types != ['']:
        config.Append('icmp-type %s' % self._Group(icmp_types))
      if self.term.icmp_code:
        config.Append('icmp-code %s' % self._Group(self.term.icmp_code))
      if self.term.ether_type:
        config.Append('ether-type %s' %
                      self._Group(self.term.ether_type))

      if self.term.traffic_type:
        config.Append('traffic-type %s' %
                      self._Group(self.term.traffic_type))

      if self.term.precedence:
        # precedence may be a single integer, or a space separated list
        policy_precedences = set()
        # precedence values may only be 0 through 7
        for precedence in self.term.precedence:
          if int(precedence) in range(0, 8):
            policy_precedences.add(precedence)
          else:
            raise PrecedenceError('Precedence value %s is out of bounds in %s' %
                                  (precedence, self.term.name))
        config.Append('precedence %s' % self._Group(sorted(policy_precedences)))

      # DSCP Match
      if self.term.dscp_match:
        if self.term_type == 'inet6':
          config.Append('traffic-class [ %s ];' % (
              ' '.join(self.term.dscp_match)))
        else:
          config.Append('dscp [ %s ];' % ' '.join(self.term.dscp_match))

      # DSCP Except
      if self.term.dscp_except:
        if self.term_type == 'inet6':
          config.Append('traffic-class-except [ %s ];' % (
              ' '.join(self.term.dscp_except)))
        else:
          config.Append('dscp-except [ %s ];' % ' '.join(self.term.dscp_except))

      if self.term.hop_limit:
        # Only generate a hop-limit if inet6, inet4 has not hop-limit.
        if self.term_type == 'inet6':
          config.Append('hop-limit %s;' % (self.term.hop_limit))

      # flexible-match
      if self.term.flexible_match_range:
        config.Append('flexible-match-range {')
        for fm_opt in self.term.flexible_match_range:
          config.Append('%s %s;' % (fm_opt[0], fm_opt[1]))
        config.Append('}')

      config.Append('}')  # end from { ... }

    ####
    # ACTIONS go below here
    ####

    # If the action is only one line, include it in the same line as "then "
    # statement.
    # For example, if the action is only accept, it should be:
    # "then accept;" rather than:
    # "then {
    #     accept;
    # }"
    #
    self.CheckTerminatingAction()
    unique_actions = set(self.extra_actions)
    if not self.term.routing_instance:
      unique_actions.update(self.term.action)
    if self.term.encapsulate:
      unique_actions.add('encapsulate')
    if len(unique_actions) <= 1:
      for action in [self.term.logging, self.term.routing_instance,
                     self.term.counter, self.term.policer, self.term.qos,
                     self.term.loss_priority, self.term.dscp_set,
                     self.term.next_ip, self.term.traffic_class_count]:
        if action:
          try:
            unique_actions.update(action)
          except TypeError:
            unique_actions.add(action)
          if len(unique_actions) > 1:
            break

    if len(unique_actions) == 1:
      # b/21795531: Juniper device treats a set of IPv4 actions differently
      # than any other actions.
      # For example, if the term is in IPv4 and the action is only discard,
      # it should be:
      # "then {
      #     discard;
      # }" rather than:
      # "then discard;"
      current_action = self.ACTIONS.get(unique_actions.pop(), 'next_ip')
      if (self.term_type == 'inet' and
          current_action in ['discard', 'reject', 'reject tcp-reset']
         ) or (self.term_type == 'inet6' and current_action in
               ['reject', 'reject tcp-reset']):
        config.Append('then {')
        config.Append('%s;' % current_action)
        config.Append('}')
      elif current_action == 'next_ip':
        self.NextIpCheck(self.term.next_ip, self.term.name)
        config.Append('then {')
        if self.term.next_ip[0].version == 4:
          config.Append('next-ip %s;' % str(self.term.next_ip[0]))
        else:
          config.Append('next-ip6 %s;' % str(self.term.next_ip[0]))
        config.Append('}')
      elif current_action == 'encapsulate':
        config.Append('then {')
        config.Append('encapsulate %s;' % str(self.term.encapsulate))
        config.Append('}')
      else:
        config.Append('then %s;' % current_action)
    elif len(unique_actions) > 1:
      config.Append('then {')
      # logging
      if self.term.logging:
        for log_target in self.term.logging:
          if str(log_target) == 'local':
            config.Append('log;')
          else:
            config.Append('syslog;')

      if self.term.routing_instance:
        config.Append('routing-instance %s;' % self.term.routing_instance)

      if self.term.counter:
        config.Append('count %s;' % self.term.counter)

      if self.term.traffic_class_count:
        config.Append('traffic-class-count %s;' % self.term.traffic_class_count)

      oid_length = 128
      if self.term.policer:
        config.Append('policer %s;' % self.term.policer)
        if len(self.term.policer) > oid_length:
          logging.warning('WARNING: %s is longer than %d bytes. Due to '
                          'limitation in JUNOS, OIDs longer than %dB can '
                          'cause SNMP timeout issues.',
                          self.term.policer, oid_length, oid_length)

      if self.term.qos:
        config.Append('forwarding-class %s;' % self.term.qos)

      if self.term.loss_priority:
        config.Append('loss-priority %s;' % self.term.loss_priority)
      if self.term.next_ip:
        self.NextIpCheck(self.term.next_ip, self.term.name)
        if self.term.next_ip[0].version == 4:
          config.Append('next-ip %s;' % str(self.term.next_ip[0]))
        else:
          config.Append('next-ip6 %s;' % str(self.term.next_ip[0]))
      if self.term.encapsulate:
        config.Append('encapsulate %s;' % str(self.term.encapsulate))
      for action in self.extra_actions:
        config.Append(action + ';')

      # If there is a routing-instance defined, skip reject/accept/etc actions.
      if not self.term.routing_instance:
        for action in self.term.action:
          config.Append(self.ACTIONS.get(action) + ';')

      # DSCP SET
      if self.term.dscp_set:
        if self.term_type == 'inet6':
          config.Append('traffic-class %s;' % self.term.dscp_set)
        else:
          config.Append('dscp %s;' % self.term.dscp_set)

      config.Append('}')  # end then{...}

    config.Append('}')  # end term accept-foo-to-bar { ... }

    return str(config)

  @staticmethod
  def NextIpCheck(next_ip, term_name):
    if len(next_ip) > 1:
      raise JuniperNextIpError('The following term has more '
                               'than one next IP value: %s' % term_name)
    if next_ip[0].num_addresses > 1:
      raise JuniperNextIpError('The following term has a subnet '
                               'instead of a host: %s' % term_name)

  def CheckTerminatingAction(self):
    action = set(self.term.action)
    if self.term.encapsulate:
      action.add(self.term.encapsulate)
    if self.term.routing_instance:
      action.add(self.term.routing_instance)
    if len(action) > 1:
      raise JuniperMultipleTerminatingActionError(
          'The following term has multiple terminating actions: %s' %
          self.term.name)

  def _MinimizePrefixes(self, include, exclude):
    """Calculate a minimal set of prefixes for Juniper match conditions.

    Args:
      include: Iterable of nacaddr objects, prefixes to match.
      exclude: Iterable of nacaddr objects, prefixes to exclude.
    Returns:
      A tuple (I,E) where I and E are lists containing the minimized
      versions of include and exclude, respectively.  The order
      of each input list is preserved.
    """
    # Remove any included prefixes that have EXACT matches in the
    # excluded list.  Excluded prefixes take precedence on the router
    # regardless of the order in which the include/exclude are applied.
    exclude_set = set(exclude)
    include_result = [ip for ip in include if ip not in exclude_set]

    # Every address match condition on a Juniper firewall filter
    # contains an implicit "0/0 except" or "0::0/0 except".  If an
    # excluded prefix is not contained within any less-specific prefix
    # in the included set, we can elide it.  In other words, if the
    # next-less-specific prefix is the implicit "default except",
    # there is no need to configure the more specific "except".
    #
    # TODO(kbrint): this could be made more efficient with a Patricia trie.
    exclude_result = []
    for exclude_prefix in exclude:
      for include_prefix in include_result:
        if exclude_prefix.subnet_of(include_prefix):
          exclude_result.append(exclude_prefix)
          break

    return include_result, exclude_result

  def _Comment(self, addr, exclude=False, line_length=132):
    """Returns address comment field if it exists.

    Args:
      addr: nacaddr.IPv4 object (?)
      exclude: bool - address excludes have different indentations
      line_length: integer - this is the length to which a comment will be
        truncated, no matter what.  ie, a 1000 character comment will be
        truncated to line_length, and then split.  if 0, the whole comment
        is kept. the current default of 132 is somewhat arbitrary.

    Returns:
      List of strings.

    Notes:
      This method tries to intelligently split long comments up.  if we've
      managed to summarize 4 /32's into a /30, each with a nacaddr text field
      of something like 'foobar N', normal concatination would make the
      resulting rendered comment look in mondrian like

                         source-address {
                             ...
                             1.1.1.0/30; /* foobar1, foobar2, foobar3, foo
      bar4 */

      b/c of the line splitting at 80 chars.  this method will split the
      comments at word breaks and make the previous example look like

                         source-address {
                              ....
                              1.1.1.0/30; /* foobar1, foobar2, foobar3,
                                          ** foobar4 */
      much cleaner.
    """
    rval = []
    if self.noverbose:
      return rval
    # indentation, for multi-line comments, ensures that subsquent lines
    # are correctly alligned with the first line of the comment.
    indentation = 0
    if exclude:
      # len('1.1.1.1/32 except;') == 21
      indentation = 21 + self._DEFAULT_INDENT
    else:
      # len('1.1.1.1/32;') == 14
      indentation = 14 + self._DEFAULT_INDENT

    # length_eol is the width of the line; b/c of the addition of the space
    # and the /* characters, it needs to be a little less than the actual width
    # to keep from wrapping
    length_eol = 77 - indentation

    if isinstance(addr, (nacaddr.IPv4, nacaddr.IPv6, summarizer.DSMNet)):
      if addr.text:

        if line_length == 0:
          # line_length of 0 means that we don't want to truncate the comment.
          line_length = len(addr.text)

        # There should never be a /* or */, but be safe and ignore those
        # comments
        if addr.text.find('/*') >= 0 or addr.text.find('*/') >= 0:
          logging.debug('Malformed comment [%s] ignoring', addr.text)
        else:

          text = addr.text[:line_length]

          comment = ' /*'
          while text:
            # split the line
            if len(text) > length_eol:
              new_length_eol = text[:length_eol].rfind(' ')
              if new_length_eol <= 0:
                new_length_eol = length_eol
            else:
              new_length_eol = length_eol

            # what line am I gunna output?
            line = comment + ' ' + text[:new_length_eol].strip()
            # truncate what's left
            text = text[new_length_eol:]
            # setup the comment and indentation for the next go-round
            comment = ' ' * indentation + '**'

            rval.append(line)

          rval[-1] += ' */'
    else:
      # should we be paying attention to any other addr type?
      logging.debug('Ignoring non IPv4 or IPv6 address: %s', addr)
    return rval

  def _Group(self, group, lc=True):
    """If 1 item return it, else return [ item1 item2 ].

    Args:
      group: a list.  could be a list of strings (protocols) or a list of
             tuples (ports)
      lc: return a lower cased result for text.  Default is True.

    Returns:
      rval: a string surrounded by '[' and '];' if len(group) > 1
            or with just ';' appended if len(group) == 1
    """

    def _FormattedGroup(el, lc=True):
      """Return the actual formatting of an individual element.

      Args:
        el: either a string (protocol) or a tuple (ports)
        lc: return lower cased result for text.  Default is True.

      Returns:
        string: either the lower()'ed string or the ports, hyphenated
                if they're a range, or by itself if it's not.
      """
      if isinstance(el, str) or isinstance(el, str):
        if lc:
          return el
        else:
          return el.lower()
      elif isinstance(el, int):
        return str(el)
      # type is a tuple below here
      elif el[0] == el[1]:
        return '%d' % el[0]
      else:
        return '%d-%d' % (el[0], el[1])

    if len(group) > 1:
      rval = '[ ' + ' '.join([_FormattedGroup(x) for x in group]) + ' ];'
    else:
      rval = _FormattedGroup(group[0]) + ';'
    return rval


class Juniper(aclgenerator.ACLGenerator):
  """JCL rendering class.

    This class takes a policy object and renders the output into a syntax
    which is understood by juniper routers.

  Attributes:
    pol: policy.Policy object
  """

  _PLATFORM = 'juniper'
  _DEFAULT_PROTOCOL = 'ip'
  _SUPPORTED_AF = frozenset(('inet', 'inet6', 'bridge', 'mixed'))
  _TERM = Term
  SUFFIX = '.jcl'

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'address',
                         'restrict_address_family',
                         'counter',
                         'destination_prefix',
                         'destination_prefix_except',
                         'dscp_except',
                         'dscp_match',
                         'dscp_set',
                         'encapsulate',
                         'ether_type',
                         'filter_term',
                         'flexible_match_range',
                         'forwarding_class',
                         'forwarding_class_except',
                         'fragment_offset',
                         'hop_limit',
                         'icmp_code',
                         'logging',
                         'loss_priority',
                         'next_ip',
                         'owner',
                         'packet_length',
                         'policer',
                         'port',
                         'precedence',
                         'protocol_except',
                         'qos',
                         'routing_instance',
                         'source_prefix',
                         'source_prefix_except',
                         'traffic_type',
                         'traffic_class_count',
                         'ttl'}
    supported_sub_tokens.update({
        'option': {
            'established',
            'first-fragment',
            'is-fragment',
            # TODO(sneakywombat): add all options to lex.
            '.*',  # make ArbitraryOptions work, yolo.
            'sample',
            'tcp-established',
            'tcp-initial',
            'inactive'}
         })
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.juniper_policies = []
    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)
      filter_name = header.FilterName(self._PLATFORM)

      # Check for the position independent options and remove them from
      # the list.
      interface_specific = 'not-interface-specific' not in filter_options[1:]
      enable_dsmo = 'enable_dsmo' in filter_options[1:]
      noverbose = 'noverbose' in filter_options[1:]
      filter_enhanced_mode = 'filter_enhanced_mode' in filter_options[1:]

      if not interface_specific:
        filter_options.remove('not-interface-specific')
      if enable_dsmo:
        filter_options.remove('enable_dsmo')
      if filter_enhanced_mode:
        filter_options.remove('filter_enhanced_mode')

      # default to inet4 filters
      filter_type = 'inet'
      if len(filter_options) > 1:
        filter_type = filter_options[1]

      if filter_type == 'mixed':
        filter_types_to_process = ['inet', 'inet6']
      else:
        filter_types_to_process = [filter_type]

      for filter_type in filter_types_to_process:

        filter_name_suffix = ''
        # If mixed filter_type, will append 4 or 6 to the filter name
        if len(filter_types_to_process) > 1:
          if filter_type == 'inet':
            filter_name_suffix = '4'
          if filter_type == 'inet6':
            filter_name_suffix = '6'

        term_names = set()
        new_terms = []
        for term in terms:

          # Ignore if the term is for a different AF
          if term.restrict_address_family and term.restrict_address_family != filter_type:
            continue

          # if inactive is set, deactivate the term and remove the option.
          if 'inactive' in term.option:
            term.inactive = True
            term.option.remove('inactive')

          term.name = self.FixTermLength(term.name)

          if term.name in term_names:
            raise JuniperDuplicateTermError('You have multiple terms named: %s' %
                                            term.name)
          term_names.add(term.name)

          term = self.FixHighPorts(term, af=filter_type)
          if not term:
            continue

          if term.expiration:
            if term.expiration <= exp_info_date:
              logging.info('INFO: Term %s in policy %s expires '
                          'in less than two weeks.', term.name, filter_name)
            if term.expiration <= current_date:
              logging.warning('WARNING: Term %s in policy %s is expired and '
                              'will not be rendered.', term.name, filter_name)
              continue
          if 'is-fragment' in term.option and filter_type == 'inet6':
            raise JuniperFragmentInV6Error('The term %s uses "is-fragment" but '
                                          'is a v6 policy.' % term.name)

          new_terms.append(self._TERM(term, filter_type, enable_dsmo, noverbose))

        self.juniper_policies.append((header, filter_name + filter_name_suffix, filter_type,
                                      interface_specific, filter_enhanced_mode, new_terms))

  def __str__(self):
    config = Config()

    for (header, filter_name, filter_type, interface_specific, filter_enhanced_mode, terms
        ) in self.juniper_policies:
      # add the header information
      config.Append('firewall {')
      config.Append('family %s {' % filter_type)
      config.Append('/*')

      # we want the acl to contain id and date tags, but p4 will expand
      # the tags here when we submit the generator, so we have to trick
      # p4 into not knowing these words.  like taking c-a-n-d-y from a
      # baby.
      for line in aclgenerator.AddRepositoryTags('** '):
        config.Append(line)
      config.Append('**')

      for comment in header.comment:
        for line in comment.split('\n'):
          config.Append('** ' + line)
      config.Append('*/')

      config.Append('replace: filter %s {' % filter_name)
      if interface_specific:
        config.Append('interface-specific;')
      if filter_enhanced_mode:
        config.Append('enhanced-mode;')

      for term in terms:
        term_str = str(term)
        if term_str:
          config.Append(term_str, verbatim=True)

      config.Append('}')  # filter { ... }
      config.Append('}')  # family inet { ... }
      config.Append('}')  # firewall { ... }

    return str(config) + '\n'


# capirca/lib/junipermsmpc.py

# Copyright 2020 Google LLC
#
# 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
#
#     https://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.
#
"""Juniper MS-MPC  generator for capirca."""

import datetime
import logging

from capirca.lib import aclgenerator
from capirca.lib import juniper
from capirca.lib import nacaddr
import six

MAX_IDENTIFIER_LEN = 55  # It is really 63, but leaving room for added chars


class Term(juniper.Term):
  """Representation of an individual Juniper MS-MPC term.

     The __str__ method must be implemented.

     Args: term policy.Term object

  """
  _PLATFORM = 'msmpc'
  _DEFAULT_INDENT = 20
  _ACTIONS = {'accept': 'accept', 'deny': 'discard', 'reject': 'reject'}
  # msmpc supports a limited number of protocol names
  # https://www.juniper.net/documentation/us/en/software/junos/security-policies/topics/ref/statement/applications-edit-protocol.html
  _SUPPORTED_PROTOCOL_NAMES = (
      'ah',
      'egp',
      'esp',
      'gre',
      'icmp',
      'icmpv6',
      'igmp',
      'ipip',
      #'node', A pseudo-protocol which may require additional handling
      'ospf',
      'pim',
      'rsvp',
      'sctp',
      'tcp',
      'udp')

  def __init__(self, term, term_type, noverbose, filter_name):
    enable_dsmo = False
    super().__init__(term, term_type, enable_dsmo, noverbose)
    self.term = term
    self.term_type = term_type
    self.noverbose = noverbose
    self.filter_name = filter_name

    for prot in self.term.protocol:
      if prot not in self._SUPPORTED_PROTOCOL_NAMES:
        loc = self.term.protocol.index(prot)
        self.term.protocol[loc] = str(self.PROTO_MAP.get(prot, prot))

  def __str__(self):
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if self._PLATFORM not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if self._PLATFORM in self.term.platform_exclude:
        return ''

    if self.enable_dsmo:
      raise NotImplementedError('enable_dsmo not implemented for msmpc')

    ret_str = juniper.Config(indent=self._DEFAULT_INDENT)

    # COMMENTS
    # this deals just fine with multi line comments, but we could probably
    # output them a little cleaner; do things like make sure the
    # len(output) < 80, etc. Note, if 'noverbose' is set for the filter, skip
    # all comment processing.
    if not self.noverbose:
      if self.term.owner:
        self.term.comment.append('Owner: %s' % self.term.owner)
      if self.term.comment:
        ret_str.Append('/*')
        for comment in self.term.comment:
          for line in comment.split('\n'):
            ret_str.Append('** ' + line)
        ret_str.Append('*/')

    # Term verbatim output - this will skip over normal term creation
    # code.  Warning generated from policy.py if appropriate.
    if self.term.verbatim:
      for next_term in self.term.verbatim:
        if next_term[0] == self._PLATFORM:
          ret_str.Append(str(next_term[1]), verbatim=True)
      return str(ret_str)

    # Determine whether there are any match conditions for the term.
    has_match_criteria = (
        self.term.address or self.term.dscp_except or self.term.dscp_match or
        self.term.destination_address or self.term.destination_port or
        self.term.destination_prefix or self.term.destination_prefix_except or
        self.term.encapsulate or self.term.ether_type or
        self.term.flexible_match_range or self.term.forwarding_class or
        self.term.forwarding_class_except or self.term.fragment_offset or
        self.term.hop_limit or self.term.next_ip or self.term.port or
        self.term.precedence or self.term.protocol or
        self.term.protocol_except or self.term.source_address or
        self.term.source_port or self.term.source_prefix or
        self.term.source_prefix_except or self.term.traffic_type or
        self.term.ttl)

    suffixes = []
    duplicate_term = False
    has_icmp = 'icmp' in self.term.protocol
    has_icmpv6 = 'icmpv6' in self.term.protocol
    has_v4_ip = self.term.GetAddressOfVersion(
        'source_address',
        self.AF_MAP.get('inet')) or self.term.GetAddressOfVersion(
            'source_address_exclude',
            self.AF_MAP.get('inet')) or self.term.GetAddressOfVersion(
                'destination_address',
                self.AF_MAP.get('inet')) or self.term.GetAddressOfVersion(
                    'destination_address_exclude', self.AF_MAP.get('inet'))
    has_v6_ip = self.term.GetAddressOfVersion(
        'source_address',
        self.AF_MAP.get('inet6')) or self.term.GetAddressOfVersion(
            'source_address_exclude',
            self.AF_MAP.get('inet6')) or self.term.GetAddressOfVersion(
                'destination_address',
                self.AF_MAP.get('inet6')) or self.term.GetAddressOfVersion(
                    'destination_address_exclude', self.AF_MAP.get('inet6'))

    if self.term_type == 'mixed':
      if not (has_v4_ip or has_v6_ip):
        suffixes = ['inet']
      elif not has_v6_ip:
        suffixes = ['inet']
      elif not has_v4_ip:
        suffixes = ['inet6']
      else:
        suffixes = ['inet', 'inet6']
        duplicate_term = True
    if not suffixes and self.term_type in ['inet', 'inet6']:
      suffixes = [self.term_type]

    for suffix in suffixes:
      if self.term_type == 'mixed' and (not (has_icmp and has_icmpv6)) and (
          has_v4_ip and has_v6_ip):
        if (has_icmp and suffix != 'inet') or (has_icmpv6 and
                                               suffix != 'inet6'):
          continue
      source_address = self.term.GetAddressOfVersion('source_address',
                                                     self.AF_MAP.get(suffix))
      source_address_exclude = self.term.GetAddressOfVersion(
          'source_address_exclude', self.AF_MAP.get(suffix))
      source_address, source_address_exclude = self._MinimizePrefixes(
          source_address, source_address_exclude)
      destination_address = self.term.GetAddressOfVersion(
          'destination_address', self.AF_MAP.get(suffix))
      destination_address_exclude = self.term.GetAddressOfVersion(
          'destination_address_exclude', self.AF_MAP.get(suffix))
      destination_address, destination_address_exclude = self._MinimizePrefixes(
          destination_address, destination_address_exclude)
      if ((not source_address) and self.term.GetAddressOfVersion(
          'source_address', self.AF_MAP.get('mixed')) and
          not source_address_exclude) or (
              (not destination_address) and self.term.GetAddressOfVersion(
                  'destination_address', self.AF_MAP.get('mixed')) and
              not destination_address_exclude):
        continue
      if ((has_icmpv6 and not has_icmp and suffix == 'inet') or
          (has_icmp and not has_icmpv6 and
           suffix == 'inet6')) and self.term_type != 'mixed':
        logging.debug(
            self.NO_AF_LOG_PROTO.substitute(
                term=self.term.name,
                proto=', '.join(self.term.protocol),
                af=suffix))
        return ''

      # NAME
      # if the term is inactive we have to set the prefix
      if self.term.inactive:
        term_prefix = 'inactive:'
      else:
        term_prefix = ''

      ret_str.Append(
          '%s term %s%s {' %
          (term_prefix, self.term.name, '-' + suffix if duplicate_term else ''))

      # We only need a "from {" clause if there are any conditions to match.
      if has_match_criteria:
        ret_str.Append('from {')
        # SOURCE ADDRESS
        if source_address or source_address_exclude:
          ret_str.Append('source-address {')
          if source_address:
            for saddr in source_address:
              for comment in self._Comment(saddr):
                ret_str.Append('%s' % comment)
              if saddr.version == 6 and 0 < saddr.prefixlen < 16:
                for saddr2 in saddr.subnets(new_prefix=16):
                  ret_str.Append('%s;' % saddr2)
              else:
                if saddr == nacaddr.IPv6('0::0/0'):
                  saddr = 'any-ipv6'
                elif saddr == nacaddr.IPv4('0.0.0.0/0'):
                  saddr = 'any-ipv4'
                ret_str.Append('%s;' % saddr)

          # SOURCE ADDRESS EXCLUDE
          if source_address_exclude:
            for ex in source_address_exclude:
              for comment in self._Comment(ex):
                ret_str.Append('%s' % comment)
              if ex.version == 6 and 0 < ex.prefixlen < 16:
                for ex2 in ex.subnets(new_prefix=16):
                  ret_str.Append('%s except;' % ex2)
              else:
                if ex == nacaddr.IPv6('0::0/0'):
                  ex = 'any-ipv6'
                elif ex == nacaddr.IPv4('0.0.0.0/0'):
                  ex = 'any-ipv4'
                ret_str.Append('%s except;' % ex)
          ret_str.Append('}')  # source-address {...}

        # DESTINATION ADDRESS
        if destination_address or destination_address_exclude:
          ret_str.Append('destination-address {')
          if destination_address:
            for daddr in destination_address:
              for comment in self._Comment(daddr):
                ret_str.Append('%s' % comment)
              if daddr.version == 6 and 0 < daddr.prefixlen < 16:
                for daddr2 in daddr.subnets(new_prefix=16):
                  ret_str.Append('%s;' % daddr2)
              else:
                if daddr == nacaddr.IPv6('0::0/0'):
                  daddr = 'any-ipv6'
                elif daddr == nacaddr.IPv4('0.0.0.0/0'):
                  daddr = 'any-ipv4'
                ret_str.Append('%s;' % daddr)

          # DESTINATION ADDRESS EXCLUDE
          if destination_address_exclude:
            for ex in destination_address_exclude:
              for comment in self._Comment(ex):
                ret_str.Append('%s' % comment)
              if ex.version == 6 and 0 < ex.prefixlen < 16:
                for ex2 in ex.subnets(new_prefix=16):
                  ret_str.Append('%s except;' % ex2)
              else:
                if ex == nacaddr.IPv6('0::0/0'):
                  ex = 'any-ipv6'
                elif ex == nacaddr.IPv4('0.0.0.0/0'):
                  ex = 'any-ipv4'
                ret_str.Append('%s except;' % ex)
          ret_str.Append('}')  # destination-address {...}

        # source prefix <except> list
        if self.term.source_prefix or self.term.source_prefix_except:
          for pfx in self.term.source_prefix:
            ret_str.Append('source-prefix-list ' + pfx + ';')
          for epfx in self.term.source_prefix_except:
            ret_str.Append('source-prefix-list ' + epfx + ' except;')

        # destination prefix <except> list
        if self.term.destination_prefix or self.term.destination_prefix_except:
          for pfx in self.term.destination_prefix:
            ret_str.Append('destination-prefix-list ' + pfx + ';')
          for epfx in self.term.destination_prefix_except:
            ret_str.Append('destination-prefix-list ' + epfx + ' except;')

        # APPLICATION
        if (self.term.source_port or self.term.destination_port or
            self.term.icmp_type or self.term.protocol):
          if hasattr(self.term, 'replacement_application_name'):
            ret_str.Append('application-sets ' +
                           self.term.replacement_application_name + '-app;')
          else:
            ret_str.Append('application-sets ' +
                           self.filter_name[:((MAX_IDENTIFIER_LEN) // 2)] +
                           self.term.name[-((MAX_IDENTIFIER_LEN) // 2):] +
                           '-app;')
        ret_str.Append('}')  # from {...}

      ret_str.Append('then {')
      # ACTION
      for action in self.term.action:
        ret_str.Append(self._ACTIONS.get(str(action)) + ';')
      if self.term.logging and 'disable' not in [
          x.value for x in self.term.logging
      ]:
        ret_str.Append('syslog;')
      ret_str.Append('}')  # then {...}
      ret_str.Append('}')  # term {...}
    return str(ret_str)


class JuniperMSMPC(aclgenerator.ACLGenerator):
  """Juniper MSMPC rendering class.

     This class takes a policy object and renders output into
     a syntax which is understood ny Juniper routers with MS-MPC cards.

     Args:
       pol: policy.Policy object
  """
  _PLATFORM = 'msmpc'
  SUFFIX = '.msmpc'
  _SUPPORTED_AF = frozenset(('inet', 'inet6', 'mixed'))
  _AF_MAP = {'inet': 4, 'inet6': 6, 'mixed': None}
  _AF_ICMP_MAP = {'icmp': 'inet', 'icmpv6': 'inet6'}
  _SUPPORTED_DIRECTION = {
      '': 'input-output',
      'ingress': 'input',
      'egress': 'output',
  }

  _OPTIONAL_SUPPORTED_KEYWORDS = frozenset([
      'expiration',
  ])

  def __init__(self, pol, exp_info):
    self.applications = {}
    super().__init__(pol, exp_info)

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {
        'destination_prefix', 'destination_prefix_except', 'icmp_code',
        'logging', 'owner', 'source_prefix', 'source_prefix_except'
    }
    supported_sub_tokens.update({
        'option': {
            'established',
            # TODO(sneakywombat): add all options to lex.
            '.*',  # make ArbitraryOptions work, yolo.
            'tcp-established',
            'inactive'
        }
    })
    return supported_tokens, supported_sub_tokens

  def _BuildPort(self, ports):
    """Transform specified ports into list and ranges.

    Args:
      ports: a policy terms list of ports

    Returns:
      port_list: list of ports and port ranges
    """
    port_list = []
    for p in ports:
      if p[0] == p[1]:
        port_list.append(str(p[0]))
      else:
        port_list.append('%s-%s' % (str(p[0]), str(p[1])))
    return port_list

  def _GenerateApplications(self, filter_name):
    target = []
    apps_set_list = []
    target.append('applications {')
    done_apps = []
    for app in sorted(self.applications[filter_name], key=lambda x: x['name']):
      app_list = []
      if app in done_apps:
        continue

      if app['protocol'] or app['sport'] or app['dport'] or app['icmp-type']:
        # generate ICMP statements
        if app['icmp-type']:
          if app['timeout']:
            timeout = app['timeout']
          else:
            timeout = 60
          num_terms = len(app['protocol']) * len(app['icmp-type'])
          apps_set_list.append('application-set ' + app['name'] + '-app {')
          for i in range(num_terms):
            apps_set_list.append('application ' + app['name'] + '-app%d' %
                                 (i + 1) + ';')
          apps_set_list.append('}')  # application-set {...}

          term_counter = 0
          for i, code in enumerate(app['icmp-type']):
            for proto in app['protocol']:
              target.append('application ' + app['name'] + '-app%d' %
                            (term_counter + 1) + ' {')
              if proto == 'icmp':
                target.append('application-protocol %s;' % proto)
              target.append('protocol %s;' % proto)
              target.append('%s-type %s;' % (proto, str(code)))
              if app['icmp-code']:
                target.append('%s-code %s;' %
                              (proto, self._Group(app['icmp-code'])))
              if int(timeout):
                target.append('inactivity-timeout %s;' % int(timeout))
              target.append('}')  # application {...}
              term_counter += 1
        # generate non-ICMP statements
        else:
          i = 1
          apps_set_list.append('application-set ' + app['name'] + '-app {')

          for proto in app['protocol'] or ['']:
            for sport in app['sport'] or ['']:
              for dport in app['dport'] or ['']:
                chunks = []
                if proto:
                  chunks.append('protocol %s;' % proto)
                if sport and ('udp' in proto or 'tcp' in proto):
                  chunks.append('source-port %s;' % sport)
                if dport and ('udp' in proto or 'tcp' in proto):
                  chunks.append('destination-port %s;' % dport)
                if app['timeout']:
                  chunks.append(' inactivity-timeout %d;' % int(app['timeout']))
                if chunks:
                  apps_set_list.append('application ' + app['name'] +
                                       '-app%d;' % i)
                  app_list.append('application ' + app['name'] + '-app%d {' % i)
                  for chunk in chunks:
                    app_list.append(chunk)
                  app_list.append('}')
                  i += 1
          apps_set_list.append('}')

        done_apps.append(app)
        if app_list:
          for item in app_list:
            target.append(item)

    for item in apps_set_list:
      target.append(item)
    target.append('}')
    # Return the output only if there is content inside of
    # the "applications {\n}" lines, otherwise return nothing.
    if len(target) > 2:
      return target
    else:
      return []

  def _TranslatePolicy(self, pol, exp_info):
    current_date = datetime.date.today()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)
    self.junipermsmpc_policies = []
    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue
      filter_options = header.FilterOptions(self._PLATFORM)
      filter_name = header.FilterName(self._PLATFORM)
      filter_options.remove(filter_name)
      filter_direction = None
      filter_type = None
      noverbose = 'noverbose' in filter_options
      self.applications[filter_name] = []

      if noverbose:
        # noverbose is a strict boolean, remove it
        # prior to iterating through the other options
        # that require additional processing.
        filter_options.remove('noverbose')

      for filter_opt in filter_options:
        # validate address families
        if filter_opt in self._SUPPORTED_AF:
          if not filter_type:
            filter_type = filter_opt
            continue
          else:
            raise ConflictingTargetOptionsError(
                'only one address family can be '
                'specified per header "%s"' % ' '.join(filter_options))
        # validate direction
        if filter_opt in self._SUPPORTED_DIRECTION.keys():
          if not filter_direction:
            filter_direction = self._SUPPORTED_DIRECTION.get(filter_opt)
            continue
          else:
            raise ConflictingTargetOptionsError('only one direction can be '
                                                'specified per header "%s"' %
                                                ' '.join(filter_options))
        raise UnsupportedHeaderError(
            'MSMPC Generator currently does not support '
            '%s as a header option "%s"' %
            (filter_opt, ' '.join(filter_options)))

      if not filter_direction:
        filter_direction = self._SUPPORTED_DIRECTION.get('')
      if not filter_type:
        filter_type = 'mixed'

      term_names = set()
      new_terms = []
      for term in terms:
        # Application sets need to be unique system-wide, so we construct
        # a name from a combination of the filter and term names, shortening
        # to the roughly half of the max identifier length for each part.
        # When shortening, we take the start of the filter name and the end of
        # the term name in a hope that we omit the most common bits
        # like -inbound and accept-.
        modified_term_name = filter_name[:(
            (MAX_IDENTIFIER_LEN) // 2)] + term.name[-(
                (MAX_IDENTIFIER_LEN) // 2):]
        if term.stateless_reply:
          logging.warning(
              'WARNING: Term %s is a stateless reply term and will not be '
              'rendered.', term.name)
          continue
        if set(['established', 'tcp-established']).intersection(term.option):
          logging.debug(
              'Skipping established term %s because MSMPC is stateful.',
              term.name)
          continue
        # if inactive is set, deactivate the term and remove the option.
        if 'inactive' in term.option:
          term.inactive = True
          term.option.remove('inactive')
        if term.name in term_names:
          raise JuniperMSMPCFilterError('Duplicate term name')
        term_names.add(term.name)
        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info(
                'INFO: Term %s in policy %s expires '
                'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning(
                'WARNING: Term %s in policy %s is expired and '
                'will not be rendered.', term.name, filter_name)
            continue
        new_term = Term(term, filter_type, noverbose, filter_name)
        new_terms.append(new_term)

        # Because MSMPC terms can contain inet and inet6 addresses. We have to
        # have ability to recover proper AF for ICMP type we need.
        # If protocol is empty or we cannot map to inet or inet6 we insert bogus
        # af_type name which will cause new_term.NormalizeIcmpTypes to fail.
        if not term.protocol:
          icmp_af_type = 'unknown_af_icmp'
        else:
          icmp_af_type = self._AF_ICMP_MAP.get(term.protocol[0],
                                               'unknown_af_icmp')
        tmp_icmptype = new_term.NormalizeIcmpTypes(term.icmp_type,
                                                   term.protocol, icmp_af_type)
        # NormalizeIcmpTypes returns [''] for empty, convert to [] for eval
        normalized_icmptype = tmp_icmptype if tmp_icmptype != [''] else []
        # rewrites the protocol icmpv6 to icmp6
        if 'icmpv6' in term.protocol:
          protocol = list(term.protocol)
          protocol[protocol.index('icmpv6')] = 'icmp6'
        else:
          protocol = term.protocol
        # MSMPC requires tcp and udp to specify ports, rather than imply all
        # ports
        if 'udp' in term.protocol or 'tcp' in term.protocol:
          if not term.source_port and not term.destination_port:
            term.destination_port = [[1, 65535]]
        new_application_set = {
            'sport': self._BuildPort(term.source_port),
            'dport': self._BuildPort(term.destination_port),
            'protocol': protocol,
            'icmp-type': normalized_icmptype,
            'icmp-code': term.icmp_code,
            'timeout': term.timeout
        }

        for application_set in self.applications[filter_name]:
          if all(
              item in list(application_set.items())
              for item in new_application_set.items()):
            new_application_set = ''
            term.replacement_application_name = application_set['name']
            break
          if (modified_term_name == application_set['name'] and
              new_application_set != application_set):
            raise ConflictingApplicationSetsError(
                'Application set %s has a conflicting entry' %
                modified_term_name)

        if new_application_set:
          new_application_set['name'] = modified_term_name
          self.applications[filter_name].append(new_application_set)

      self.junipermsmpc_policies.append(
          (header, filter_name, filter_direction, new_terms))

  def _Group(self, group, lc=True):
    """If 1 item return it, else return [ item1 item2 ].

    Args:
      group: a list.  could be a list of strings (protocols) or a list of tuples
        (ports)
      lc: return a lower cased result for text.  Default is True.

    Returns:
      rval: a string surrounded by '[' and '];' if len(group) > 1
            or with just ';' appended if len(group) == 1
    """

    def _FormattedGroup(el, lc=True):
      """Return the actual formatting of an individual element.

      Args:
        el: either a string (protocol) or a tuple (ports)
        lc: return lower cased result for text.  Default is True.

      Returns:
        string: either the lower()'ed string or the ports, hyphenated
                if they're a range, or by itself if it's not.
      """
      if isinstance(el, str):
        if not lc:
          return el
        else:
          return el.lower()
      elif isinstance(el, int):
        return str(el)
      # type is a tuple below here
      elif el[0] == el[1]:
        return '%d' % el[0]
      else:
        return '%d-%d' % (el[0], el[1])

    if len(group) > 1:
      rval = '[ ' + ' '.join([_FormattedGroup(x, lc=lc) for x in group]) + ' ];'
    else:
      rval = _FormattedGroup(group[0], lc=lc) + ';'
    return rval

  def __str__(self):
    target = juniper.Config()
    for (header, filter_name, filter_direction,
         terms) in self.junipermsmpc_policies:
      target.Append('groups {')
      target.Append('replace:')
      target.Append('/*')

      # we want the acl to contain id and date tags, but p4 will expand
      # the tags here when we submit the generator, so we have to trick
      # p4 into not knowing these words.  like taking c-a-n-d-y from a
      # baby.
      for line in aclgenerator.AddRepositoryTags('** '):
        target.Append(line)
      target.Append('**')

      for comment in header.comment:
        for line in comment.split('\n'):
          target.Append('** ' + line)
      target.Append('*/')

      target.Append('%s {' % filter_name)
      target.Append('services {')
      target.Append('stateful-firewall {')
      target.Append('rule %s {' % filter_name)
      target.Append('match-direction %s;' % filter_direction)
      for term in terms:
        term_str = str(term)
        if term_str:
          target.Append(term_str, verbatim=True)
      target.Append('}')  # rule { ... }
      target.Append('}')  # stateful-firewall { ... }
      target.Append('}')  # services { ... }
      for line in self._GenerateApplications(filter_name):
        target.Append(line)
      target.Append('}')  # filter_name { ... }
      target.Append('}')  # groups { ... }
      target.Append('apply-groups %s;' % filter_name)
    return str(target) + '\n'


class Error(Exception):
  pass


class JuniperMSMPCFilterError(Error):
  pass


class ConflictingApplicationSetsError(Error):
  pass


class ConflictingTargetOptionsError(Error):
  pass


class UnsupportedHeaderError(Error):
  pass


# capirca/lib/junipersrx.py

# Copyright 2011 Google 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.
#

"""SRX generator."""
# pylint: disable=super-init-not-called


import collections
import copy
import datetime
import itertools

from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import nacaddr
import six


ICMP_TERM_LIMIT = 8


def JunipersrxList(name, data):
  return '%s [ %s ];' % (name, ' '.join(data))


class Error(Exception):
  """generic error class."""


class UnsupportedFilterError(Error):
  pass


class UnsupportedHeaderError(Error):
  pass


class SRXDuplicateTermError(Error):
  pass


class SRXVerbatimError(Error):
  pass


class SRXOptionError(Error):
  pass


class MixedAddrBookTypesError(Error):
  pass


class ConflictingTargetOptionsError(Error):
  pass


class ConflictingApplicationSetsError(Error):
  pass


class IndentList(list):

  def __init__(self, indent, *args, **kwargs):
    self._indent = indent
    super().__init__(*args, **kwargs)

  def IndentAppend(self, size, data):
    self.append('%s%s' % (self._indent * size, data))


class Term(aclgenerator.Term):
  """Representation of an individual SRX term.

     This is mostly useful for the __str__() method.

     Args:
       obj: a policy.Term object
       filter_options: list of remaining target options (zones)
  """

  ACTIONS = {'accept': 'permit',
             'deny': 'deny',
             'reject': 'reject',
             'count': 'count',
             'log': 'log',
             'expresspath': 'services-offload',
             'dscp': 'dscp'}

  def __init__(self, term, from_zone, to_zone, expresspath=False, verbose=True):
    super().__init__(term)
    self.term = term
    self.from_zone = from_zone
    self.to_zone = to_zone
    self.verbose = verbose
    if expresspath:
      self.term.action = [
          a.replace('accept', 'expresspath') for a in self.term.action]

  def __str__(self):
    """Render config output from this term object."""
    ret_str = IndentList(JuniperSRX.INDENT)

    # COMMENTS
    comment_max_width = 68
    if self.term.owner and self.verbose:
      self.term.comment.append('Owner: %s' % self.term.owner)
    comments = aclgenerator.WrapWords(self.term.comment, comment_max_width)
    if comments and comments[0] and self.verbose:
      ret_str.IndentAppend(3, '/*')
      for line in comments:
        ret_str.IndentAppend(3, line)
      ret_str.IndentAppend(3, '*/')

    ret_str.IndentAppend(3, 'policy ' + self.term.name + ' {')
    ret_str.IndentAppend(4, 'match {')
    # SOURCE-ADDRESS
    if self.term.source_address:
      saddr_check = set()
      for saddr in self.term.source_address:
        saddr_check.add(saddr.parent_token)
      saddr_check = sorted(saddr_check)
      ret_str.IndentAppend(5, JunipersrxList('source-address', saddr_check))
    else:
      ret_str.IndentAppend(5, 'source-address any;')

    # DESTINATION-ADDRESS
    if self.term.destination_address:
      daddr_check = []
      for daddr in self.term.destination_address:
        daddr_check.append(daddr.parent_token)
      daddr_check = set(daddr_check)
      daddr_check = list(daddr_check)
      daddr_check.sort()
      ret_str.IndentAppend(5, JunipersrxList('destination-address',
                                             daddr_check))
    else:
      ret_str.IndentAppend(5, 'destination-address any;')

    # APPLICATION
    if (not self.term.source_port and not self.term.destination_port and not
        self.term.icmp_type and not self.term.protocol):
      ret_str.IndentAppend(5, 'application any;')
    else:
      if hasattr(self.term, 'replacement_application_name'):
        ret_str.IndentAppend(5, 'application ' +
                             self.term.replacement_application_name + '-app;')
      else:
        ret_str.IndentAppend(5, 'application ' + self.term.name + '-app;')

    # DSCP MATCH
    if self.term.dscp_match:
      ret_str.IndentAppend(5, JunipersrxList('dscp', self.term.dscp_match))

    # DSCP EXCEPT
    if self.term.dscp_except:
      ret_str.IndentAppend(5, JunipersrxList('dscp-except',
                                             self.term.dscp_except))

    ret_str.IndentAppend(4, '}')

    # ACTIONS
    for action in self.term.action:
      ret_str.IndentAppend(4, 'then {')

      # VPN target can be only specified when ACTION is accept
      if str(action) == 'accept' and self.term.vpn:
        ret_str.IndentAppend(5, self.ACTIONS.get(
            str(action)) + ' {')
        ret_str.IndentAppend(6, 'tunnel {')
        ret_str.IndentAppend(7, 'ipsec-vpn %s;' % self.term.vpn[0])
        if self.term.vpn[1]:
          ret_str.IndentAppend(7, 'pair-policy %s;' % self.term.vpn[1])

        ret_str.IndentAppend(6, '}')
        ret_str.IndentAppend(5, '}')
      else:
        ret_str.IndentAppend(5, self.ACTIONS.get(str(action)) + ';')

      # DSCP SET
      if self.term.dscp_set:
        ret_str.IndentAppend(5, 'dscp ' + self.term.dscp_set + ';')

      # LOGGING
      if self.term.logging:
        ret_str.IndentAppend(5, 'log {')
        for log_target in self.term.logging:
          if str(log_target) == 'log-both':
            ret_str.IndentAppend(6, 'session-init;')
            ret_str.IndentAppend(6, 'session-close;')
          else:
            if str(action) == 'accept':
              ret_str.IndentAppend(6, 'session-close;')
            else:
              ret_str.IndentAppend(6, 'session-init;')
        ret_str.IndentAppend(5, '}')

      ret_str.IndentAppend(4, '}')

      ret_str.IndentAppend(3, '}')

    return '\n'.join(ret_str)

  def _Group(self, group):
    """If 1 item return it, else return [ item1 item2 ].

    Args:
      group: a list.  could be a list of strings (protocols) or a list of
             tuples (ports)

    Returns:
      rval: a string surrounded by '[' and '];' if len(group) > 1
            or with just ';' appended if len(group) == 1
    """

    def _FormattedGroup(el):
      """Return the actual formatting of an individual element.

      Args:
        el: either a string (protocol) or a tuple (ports)

      Returns:
        string: either the lower()'ed string or the ports, hyphenated
                if they're a range, or by itself if it's not.
      """
      if isinstance(el, str):
        return el.lower()
      elif isinstance(el, int):
        return str(el)
      # type is a tuple below here
      elif el[0] == el[1]:
        return '%d' % el[0]
      else:
        return '%d-%d' % (el[0], el[1])

    if len(group) > 1:
      rval = '[ ' + ' '.join([_FormattedGroup(x) for x in group]) + ' ];'
    else:
      rval = _FormattedGroup(group[0]) + ';'
    return rval


class JuniperSRX(aclgenerator.ACLGenerator):
  """SRX rendering class.

     This class takes a policy object and renders the output into a syntax
     which is understood by SRX firewalls.

     Args:
       pol: policy.Policy object
  """

  _PLATFORM = 'srx'
  SUFFIX = '.srx'
  _SUPPORTED_AF = set(('inet', 'inet6', 'mixed'))
  _ZONE_ADDR_BOOK = 'address-book-zone'
  _GLOBAL_ADDR_BOOK = 'address-book-global'
  _ADDRESSBOOK_TYPES = set((_ZONE_ADDR_BOOK, _GLOBAL_ADDR_BOOK))
  _EXPRESSPATH = 'expresspath'
  _NOVERBOSE = 'noverbose'
  _SUPPORTED_TARGET_OPTIONS = set((_ZONE_ADDR_BOOK, _GLOBAL_ADDR_BOOK,
                                   _EXPRESSPATH, _NOVERBOSE))

  _AF_MAP = {'inet': (4,),
             'inet6': (6,),
             'mixed': (4, 6)}
  _AF_ICMP_MAP = {'icmp': 'inet',
                  'icmpv6': 'inet6'}
  INDENT = '    '
  _MAX_HEADER_COMMENT_LENGTH = 71
  # The SRX platform is limited in how many IP addresses can be used in
  # a single policy.
  _ADDRESS_LENGTH_LIMIT = 1023
  # IPv6 are 32 bytes compared to IPv4, this is used as a multiplier.
  _IPV6_SIZE = 4

  def __init__(self, pol, exp_info):
    self.srx_policies = []
    self.addressbook = collections.OrderedDict()
    self.applications = []
    self.ports = []
    self.from_zone = ''
    self.to_zone = ''
    self.addr_book_type = set()
    super().__init__(pol, exp_info)

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'dscp_except',
                         'dscp_match',
                         'dscp_set',
                         'logging',
                         'option',
                         'owner',
                         'timeout',
                         'verbatim',
                         'vpn'}

    supported_sub_tokens.update(
        {'action': {'accept', 'deny', 'reject', 'count', 'log', 'dscp'},
        })
    del supported_sub_tokens['option']
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    # pylint: disable=attribute-defined-outside-init
    """Transform a policy object into a JuniperSRX object.

    Args:
      pol: policy.Policy object
      exp_info: print a info message when a term is set to expire
                in that many weeks

    Raises:
      UnsupportedFilterError: An unsupported filter was specified
      UnsupportedHeaderError: A header option exists that is not
                              understood/usable
      SRXDuplicateTermError: Two terms were found with same name in same filter
      ConflictingTargetOptionsError: Two target options are conflicting
                                     in the header
      MixedAddrBookTypesError: Global and Zone address books in the same policy
      ConflictingApplicationSetsError: When two duplicate named terms have
                                       conflicting application entries
    """
    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)

      verbose = True
      if self._NOVERBOSE in filter_options[4:]:
        verbose = False

      # TODO(robankeny): Clean up option section.
      if (len(filter_options) < 4 or filter_options[0] != 'from-zone' or
          filter_options[2] != 'to-zone'):
        raise UnsupportedFilterError('SRX filter arguments must specify '
                                     'from-zone and to-zone.')

      # check if to-zone is not a supported target option
      if filter_options[1] in self._SUPPORTED_TARGET_OPTIONS:
        raise UnsupportedFilterError('to-zone %s cannot be the same as any '
                                     'valid SRX target-options' %
                                     (filter_options[1]))
      else:
        self.from_zone = filter_options[1]

      # check if from-zone is not a supported target option
      if filter_options[3] in self._SUPPORTED_TARGET_OPTIONS:
        raise UnsupportedFilterError('from-zone %s cannot be the same as any '
                                     'valid SRX target-options' %
                                     (filter_options[3]))
      else:
        self.to_zone = filter_options[3]

      # variables used to collect target-options and set defaults
      filter_type = ''

      # parse srx target options
      extra_options = filter_options[4:]
      if self._ADDRESSBOOK_TYPES.issubset(extra_options):
        raise ConflictingTargetOptionsError(
            'only one address-book-type can '
            'be specified per header "%s"' % ' '.join(filter_options))
      else:
        address_book_type = set(
            [self._ZONE_ADDR_BOOK,
             self._GLOBAL_ADDR_BOOK]).intersection(extra_options)
        if not address_book_type:
          address_book_type = {self._GLOBAL_ADDR_BOOK}
        self.addr_book_type.update(address_book_type)
        if len(self.addr_book_type) > 1:
          raise MixedAddrBookTypesError(
              'Global and Zone address-book-types cannot '
              'be used in the same policy')
        if self.from_zone == 'all' and self.to_zone == 'all':
          if self._ZONE_ADDR_BOOK in self.addr_book_type:
            raise UnsupportedFilterError('Zone address books cannot be used '
                                         'with a global policy.')
        elif self.from_zone == 'all' or self.to_zone == 'all':
          raise UnsupportedFilterError('The zone name all is reserved for '
                                       'global policies.')

      if self._EXPRESSPATH in filter_options[4:]:
        self.expresspath = True
      else:
        self.expresspath = False

      for filter_opt in filter_options[4:]:
        # validate address families
        if filter_opt in self._SUPPORTED_AF:
          if not filter_type:
            filter_type = filter_opt
          else:
            raise ConflictingTargetOptionsError(
                'only one address family can be '
                'specified per header "%s"' % ' '.join(filter_options))
        elif filter_opt in self._SUPPORTED_TARGET_OPTIONS:
          continue
        else:
          raise UnsupportedHeaderError(
              'SRX Generator currently does not support '
              '%s as a header option "%s"' %
              (filter_opt, ' '.join(filter_options)))

      # if address-family and address-book-type have not been set then default
      if not filter_type:
        filter_type = 'mixed'

      term_dup_check = set()

      new_terms = []
      self._FixLargePolices(terms, filter_type)
      for term in terms:
        # Only generate the term if it's for the appropriate platform.
        if term.platform:
          if self._PLATFORM not in term.platform:
            continue
        if term.platform_exclude:
          if self._PLATFORM in term.platform_exclude:
            continue

        if term.stateless_reply:
          logging.warning(
              "WARNING: Term %s in policy %s>%s is a stateless reply "
              "term and will not be rendered.", term.name, self.from_zone,
              self.to_zone)
          continue
        if set(['established', 'tcp-established']).intersection(term.option):
          logging.debug('Skipping established term %s because SRX is stateful.',
                        term.name)
          continue
        term.name = self.FixTermLength(term.name)
        if term.name in term_dup_check:
          raise SRXDuplicateTermError('You have a duplicate term: %s'
                                      % term.name)
        term_dup_check.add(term.name)

        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s>%s expires '
                         'in less than two weeks.', term.name, self.from_zone,
                         self.to_zone)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s>%s is expired.',
                            term.name, self.from_zone, self.to_zone)
            continue

        # SRX address books leverage network token names for IPs.
        # When excluding addresses, we lose those distinct names so we need
        # to create a new unique name based off the term name before excluding.
        if term.source_address_exclude:
          # If we have a naked source_exclude, we need something to exclude from
          if not term.source_address:
            term.source_address = [nacaddr.IP('0.0.0.0/0',
                                              term.name.upper(),
                                              term.name.upper())]
          # Use the term name as the token & parent_token
          new_src_parent_token = term.name.upper() + '_SRC_EXCLUDE'
          new_src_token = new_src_parent_token
          for i in term.source_address_exclude:
            term.source_address = nacaddr.RemoveAddressFromList(
                term.source_address, i)
            for j in term.source_address:
              j.token = new_src_token
              j.parent_token = new_src_parent_token

        if term.destination_address_exclude:
          if not term.destination_address:
            term.destination_address = [nacaddr.IP('0.0.0.0/0',
                                                   term.name.upper(),
                                                   term.name.upper())]
          new_dst_parent_token = term.name.upper() + '_DST_EXCLUDE'
          new_dst_token = new_dst_parent_token
          for i in term.destination_address_exclude:
            term.destination_address = nacaddr.RemoveAddressFromList(
                term.destination_address, i)
            for j in term.destination_address:
              j.token = new_dst_token
              j.parent_token = new_dst_parent_token

        # SRX policies are controlled by addresses that are used within, so
        # policy can be at the same time inet and inet6.
        if self._GLOBAL_ADDR_BOOK in self.addr_book_type:
          for zone in self.addressbook:
            for unused_name, ips in sorted(
                self.addressbook[zone].items()):
              ips = [i for i in ips]
              if term.source_address == ips:
                term.source_address = ips
              if term.destination_address == ips:
                term.destination_address = ips

        # Filter source_address based on filter_type & add to address book
        if term.source_address:
          valid_addrs = []
          for addr in term.source_address:
            if addr.version in self._AF_MAP[filter_type]:
              valid_addrs.append(addr)
          if not valid_addrs:
            logging.warning(
                'WARNING: Term %s has 0 valid source IPs, skipping.', term.name)
            continue
          term.source_address = valid_addrs
          for addr in term.source_address:
            self._BuildAddressBook(self.from_zone, addr)

        # Filter destination_address based on filter_type & add to address book
        if term.destination_address:
          valid_addrs = []
          for addr in term.destination_address:
            if addr.version in self._AF_MAP[filter_type]:
              valid_addrs.append(addr)
          if not valid_addrs:
            logging.warning(
                'WARNING: Term %s has 0 valid destination IPs, skipping.',
                term.name)
            continue
          term.destination_address = valid_addrs
          for addr in term.destination_address:
            self._BuildAddressBook(self.to_zone, addr)

        new_term = Term(term, self.from_zone, self.to_zone, self.expresspath,
                        verbose)
        new_terms.append(new_term)

        # Because SRX terms can contain inet and inet6 addresses. We have to
        # have ability to recover proper AF for ICMP type we need.
        # If protocol is empty or we cannot map to inet or inet6 we insert bogus
        # af_type name which will cause new_term.NormalizeIcmpTypes to fail.
        if not term.protocol:
          icmp_af_type = 'unknown_af_icmp'
        else:
          icmp_af_type = self._AF_ICMP_MAP.get(
              term.protocol[0], 'unknown_af_icmp')
        tmp_icmptype = new_term.NormalizeIcmpTypes(
            term.icmp_type, term.protocol, icmp_af_type)
        # NormalizeIcmpTypes returns [''] for empty, convert to [] for eval
        normalized_icmptype = tmp_icmptype if tmp_icmptype != [''] else []
        # rewrites the protocol icmpv6 to icmp6
        if 'icmpv6' in term.protocol:
          protocol = list(term.protocol)
          protocol[protocol.index('icmpv6')] = 'icmp6'
        else:
          protocol = term.protocol
        new_application_set = {'sport': self._BuildPort(term.source_port),
                               'dport': self._BuildPort(term.destination_port),
                               'protocol': protocol,
                               'icmp-type': normalized_icmptype,
                               'timeout': term.timeout}

        for application_set in self.applications:
          if all(item in list(application_set.items()) for item in
                 new_application_set.items()):
            new_application_set = ''
            term.replacement_application_name = application_set['name']
            break
          if (term.name == application_set['name'] and
              new_application_set != application_set):
            raise ConflictingApplicationSetsError(
                'Application set %s has a conflicting entry' % term.name)

        if new_application_set:
          new_application_set['name'] = term.name
          self.applications.append(new_application_set)

      self.srx_policies.append((header, new_terms, filter_options))

  def _FixLargePolices(self, terms, address_family):
    """Loops over all terms finding terms exceeding SRXs policy limit.

    Args:
      terms: List of terms from a policy.
      address_family: Tuple containing address family versions.

    See the following URL for more information
    http://www.juniper.net/techpubs/en_US/junos12.1x44/topics/reference/
    general/address-address-sets-limitations.html
    """

    def Chunks(l):
      """Splits a list of IP addresses into smaller lists based on byte size."""
      return_list = [[]]
      counter = 0
      index = 0
      for i in l:
        # Size is split in half due to the max size being a sum of src and dst.
        if counter > (self._ADDRESS_LENGTH_LIMIT/2):
          counter = 0
          index += 1
          return_list.append([])
        if i.version == 6:
          counter += self._IPV6_SIZE
        else:
          counter += 1
        return_list[index].append(i)
      return return_list

    expanded_terms = []
    for term in terms:
      if (term.AddressesByteLength(
          self._AF_MAP[address_family]) > self._ADDRESS_LENGTH_LIMIT):
        logging.warning('LARGE TERM ENCOUNTERED')
        src_chunks = Chunks(term.source_address)
        counter = 0
        for chunk in src_chunks:
          for ip in chunk:
            ip.parent_token = 'src_' + term.name + str(counter)
          counter += 1
        dst_chunks = Chunks(term.destination_address)
        counter = 0
        for chunk in dst_chunks:
          for ip in chunk:
            ip.parent_token = 'dst_' + term.name + str(counter)
          counter += 1

        src_dst_products = itertools.product(src_chunks, dst_chunks)
        counter = 0
        for src_dst_list in src_dst_products:
          new_term = copy.copy(term)
          new_term.source_address = src_dst_list[0]
          new_term.destination_address = src_dst_list[1]
          new_term.name = new_term.name + '_' + str(counter)
          expanded_terms.append(new_term)
          counter += 1
      else:
        expanded_terms.append(term)
    if expanded_terms:
      del terms[:]
      terms.extend(expanded_terms)

  def _BuildAddressBook(self, zone, address):
    """Create the address book configuration entries.

    Args:
      zone: the zone these objects will reside in
      address: a naming library address object
    """
    if zone not in self.addressbook:
      self.addressbook[zone] = collections.defaultdict(list)
    name = address.parent_token
    for ip in self.addressbook[zone][name]:
      if ip.supernet_of(address):
        return
      if address.supernet_of(ip):
        for index, ip_addr in enumerate(self.addressbook[zone][name]):
          if ip_addr == ip:
            self.addressbook[zone][name][index] = address
        return
    self.addressbook[zone][name].append(address)

  def _SortAddressBookNumCheck(self, item):
    """Used to give a natural order to the list of acl entries.

    Args:
      item: string of the address book entry name

    Returns:
      returns the characters and number
    """

    item_list = item.split('_')
    num = item_list.pop(-1)
    if isinstance(item_list[-1], int):
      set_number = item_list.pop(-1)
      num = int(set_number) * 1000 + int(num)
    alpha = '_'.join(item_list)
    if num:
      return (alpha, int(num))
    return (alpha, 0)

  def _BuildPort(self, ports):
    """Transform specified ports into list and ranges.

    Args:
      ports: a policy terms list of ports

    Returns:
      port_list: list of ports and port ranges
    """
    port_list = []
    for i in ports:
      if i[0] == i[1]:
        port_list.append(str(i[0]))
      else:
        port_list.append('%s-%s' % (str(i[0]), str(i[1])))
    return port_list

  def _GenerateAddressBook(self):
    """Creates address book."""
    target = IndentList(self.INDENT)

    # create address books if address-book-type set to global
    if self._GLOBAL_ADDR_BOOK in self.addr_book_type:
      global_address_book = collections.defaultdict(list)

      target.IndentAppend(1, 'replace: address-book {')
      target.IndentAppend(2, 'global {')
      for zone in self.addressbook:
        for group in self.addressbook[zone]:
          for address in self.addressbook[zone][group]:
            global_address_book[group].append(address)
      names = sorted(global_address_book.keys())
      for name in names:
        counter = 0
        ips = nacaddr.SortAddrList(global_address_book[name])
        ips = nacaddr.CollapseAddrList(ips)
        global_address_book[name] = ips
        for ip in ips:
          target.IndentAppend(4, 'address ' + name + '_' + str(counter) + ' ' +
                              str(ip) + ';')
          counter += 1

      for group in sorted(global_address_book.keys()):
        target.IndentAppend(4, 'address-set ' + group + ' {')
        counter = 0
        for unused_addr in global_address_book[group]:
          target.IndentAppend(5, 'address ' + group + '_' + str(counter) + ';')
          counter += 1
        target.IndentAppend(4, '}')

      target.IndentAppend(2, '}')
      target.IndentAppend(1, '}')

    else:
      target.IndentAppend(1, 'zones {')
      for zone in self.addressbook:
        target.IndentAppend(2, 'security-zone ' + zone + ' {')
        target.IndentAppend(3, 'replace: address-book {')

        # building individual addresses
        groups = sorted(self.addressbook[zone])
        for group in groups:
          ips = nacaddr.SortAddrList(self.addressbook[zone][group])
          ips = nacaddr.CollapseAddrList(ips)
          self.addressbook[zone][group] = ips
          count = 0
          for address in self.addressbook[zone][group]:
            target.IndentAppend(4, 'address ' + group + '_' + str(count) +
                                ' ' + str(address) + ';')
            count += 1

        # building address-sets
        for group in groups:
          target.IndentAppend(4, 'address-set ' + group + ' {')
          count = 0
          for address in self.addressbook[zone][group]:
            target.IndentAppend(5, 'address ' + group + '_' + str(count) + ';')
            count += 1

          target.IndentAppend(4, '}')
        target.IndentAppend(3, '}')
        target.IndentAppend(2, '}')
      target.IndentAppend(1, '}')

    return target

  def _GenerateApplications(self):
    target = IndentList(self.INDENT)
    apps_set_list = IndentList(self.INDENT)
    target.append('replace: applications {')
    done_apps = []
    for app in sorted(self.applications, key=lambda x: x['name']):
      app_list = IndentList(self.INDENT)
      if app in done_apps:
        continue

      if app['protocol'] or app['sport'] or app['dport'] or app['icmp-type']:
        # generate ICMP statements
        if app['icmp-type']:
          if app['timeout']:
            timeout = app['timeout']
          else:
            timeout = 60
          # SRX has a limit of 8 terms per application. To get around this,
          # we use application sets with applications that contain the terms
          # we need.
          num_terms = len(app['protocol']) * len(app['icmp-type'])
          if num_terms > ICMP_TERM_LIMIT:
            target.IndentAppend(1, 'application-set ' + app['name'] + '-app {')
            for i in range(num_terms):
              target.IndentAppend(
                  2, 'application ' + app['name'] + '-app%d' % (i + 1) + ';')
            target.IndentAppend(1, '}')
          else:
            target.IndentAppend(1, 'application ' + app['name'] + '-app {')

          term_counter = 0
          for i, code in enumerate(app['icmp-type']):
            for proto in app['protocol']:
              # if we have more than 8 (ICMP_TERM_LIMIT) terms, we use an app
              # for each term.
              if num_terms > ICMP_TERM_LIMIT:
                target.IndentAppend(
                    1, 'application ' + app['name'] + '-app%d' %
                    (term_counter + 1) + ' {')
                target.IndentAppend(
                    2, 'term t1 protocol %s %s-type %s inactivity-timeout %d;' %
                    (proto, proto, str(code), int(timeout)))
                target.IndentAppend(1, '}')
              else:
                target.IndentAppend(
                    2,
                    'term t%d protocol %s %s-type %s inactivity-timeout %d;' %
                    (i + 1, proto, proto, str(code), int(timeout)))
              term_counter += 1
          if num_terms < ICMP_TERM_LIMIT:
            target.IndentAppend(1, '}')
        # generate non-ICMP statements
        else:
          i = 1
          apps_set_list.IndentAppend(1, 'application-set ' +
                                     app['name'] + '-app {')

          for proto in app['protocol'] or ['']:
            for sport in app['sport'] or ['']:
              for dport in app['dport'] or ['']:
                chunks = []
                if proto:
                  # SRX does not like proto vrrp
                  if proto == 'vrrp':
                    proto = '112'
                  chunks.append(' protocol %s' % proto)
                if sport:
                  chunks.append(' source-port %s' % sport)
                if dport:
                  chunks.append(' destination-port %s' % dport)
                if app['timeout']:
                  chunks.append(' inactivity-timeout %d' % int(app['timeout']))
                if chunks:
                  apps_set_list.IndentAppend(
                      2, 'application ' + app['name'] + '-app%d;' % i
                  )
                  app_list.IndentAppend(
                      1, 'application ' + app['name'] + '-app%d {' % i
                  )

                  app_list.IndentAppend(2, 'term t%d' % i +
                                        ''.join(chunks) + ';')
                  app_list.IndentAppend(1, '}')
                  i += 1
          apps_set_list.IndentAppend(1, '}')

        done_apps.append(app)
        if app_list:
          target.extend(app_list)


    if len(done_apps) == 0:
      target.clear()
      target.append('delete: applications;')
      return target

    target.extend(apps_set_list)
    target.append('}\n')
    return target

  def __str__(self):
    """Render the output of the JuniperSRX policy into config."""
    target = IndentList(self.INDENT)
    target.append('security {')

    # ADDRESSBOOK
    target.extend(self._GenerateAddressBook())

    # POLICIES
    target.IndentAppend(1, '/*')
    target.extend(aclgenerator.AddRepositoryTags(self.INDENT * 1))
    target.IndentAppend(1, '*/')

    target.IndentAppend(1, 'replace: policies {')

    for (header, terms, filter_options) in self.srx_policies:
      if self._NOVERBOSE not in filter_options[4:]:
        target.IndentAppend(2, '/*')
        target.extend([self.INDENT * 2 + line for line in
                       aclgenerator.WrapWords(header.comment,
                                              self._MAX_HEADER_COMMENT_LENGTH)])
        target.IndentAppend(2, '*/')

      # ZONE DIRECTION
      if filter_options[1] == 'all' and filter_options[3] == 'all':
        target.IndentAppend(2, 'global {')
      else:
        target.IndentAppend(2, 'from-zone ' + filter_options[1] +
                            ' to-zone ' + filter_options[3] + ' {')

      # GROUPS
      if header.apply_groups:
        target.IndentAppend(3, JunipersrxList('apply-groups',
                                              header.apply_groups))
      # GROUPS EXCEPT
      if header.apply_groups_except:
        target.IndentAppend(
            3,
            JunipersrxList('apply-groups-except', header.apply_groups_except)
        )
      for term in terms:
        str_result = str(term)
        if str_result:
          target.append(str_result)
      target.IndentAppend(2, '}')
    target.IndentAppend(1, '}')
    target.append('}')

    # APPLICATIONS
    target.extend(self._GenerateApplications())

    return '\n'.join(target)


# capirca/lib/k8s.py

# Copyright 2021 Google 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.
#
"""Kubernetes NetworkPolicy resource generator.

More information about Kubernetes NetworkPolicy:
https://kubernetes.io/docs/concepts/services-networking/network-policies/
https://kubernetes.io/docs/tasks/administer-cluster/declare-network-policy/
"""

import copy
import datetime
import logging
import re

from capirca.lib import aclgenerator
import yaml


class Error(Exception):
  """Generic error class."""


class K8sNetworkPolicyError(Error):
  """Raised with problems in formatting for Kubernetes NetworkPolicies."""


class ExceededAttributeCountError(Error):
  """Raised when the total attribute count of a policy is above the maximum."""


def IsDefaultDeny(term):
  """Returns true if a term is a default deny without IPs, ports, etc."""
  skip_attrs = [
      'flattened', 'flattened_addr', 'flattened_saddr', 'flattened_daddr',
      'action', 'comment', 'name', 'logging', 'direction'
  ]
  if 'deny' not in term.action:
    return False
  # This lc will look through all methods and attributes of the object.
  # It returns only the attributes that need to be looked at to determine if
  # this is a default deny.
  for i in [
      a for a in dir(term) if not a.startswith('__') and a.islower() and
      not callable(getattr(term, a))
  ]:
    if i in skip_attrs:
      continue
    v = getattr(term, i)
    if isinstance(v, str) and v:
      return False
    if isinstance(v, list) and v:
      return False

  return True


def GetNextPriority(priority):
  """Get the priority for the next rule."""
  return priority


class Term(aclgenerator.Term):
  """Creates the term for the Kubernetes NetworkPolicy."""

  _API_VERSION = 'networking.k8s.io/v1'
  _RESOURCE_KIND = 'NetworkPolicy'
  # Policy rule name has to match specific RE:
  # No more than 253 characters, beginning and ending
  # with a lowercase alphanumeric character with dashes, dots, and lowercase
  # alphanumerics between.
  # Details: https://kubernetes.io/docs/concepts/overview/working-with-objects/names
  _TERM_NAME_RE = re.compile(r'^[a-z0-9]([a-z0-9-\.]){0,251}[a-z0-9]$')
  _TERM_MAX_LENGTH = 253

  # Protocols allowed are only tcp/udp/sctp
  # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#networkpolicyport-v1-networking-k8s-io
  PROTO_MAP = {
      'tcp': 6,
      'udp': 17,
      'sctp': 132,
      'all': -1,  # Used for default deny
  }

  def __init__(self, term):
    super().__init__(term)
    self.term = term

    if 'deny' in self.term.action:
      if IsDefaultDeny(term):
        return
      else:
        raise K8sNetworkPolicyError(
            'Kubernetes NetworkPolicy does not support explicit deny terms.')

    self._validateDirection()

    if self.term.source_port:
      raise K8sNetworkPolicyError(
          'Kubernetes NetworkPolicy does not support source port restrictions.')

    # Raise an error if the flattening of address exclusions would result in
    # overly broad network access control policies
    if (self.term.source_address_exclude and self.term.source_address or
        self.term.destination_address_exclude and
        self.term.destination_address):
      self.term.FlattenAll()
      if not self.term.source_address and self.term.direction == 'INGRESS':
        logging.error(
            'Kubernetes NetworkPolicy term %s no longer contains any source '
            'addresses after the prefixes in source_address_exclude were '
            'removed. Not rendering term.', self.term.name)
        self.term = None
        return

      if not self.term.destination_address and self.term.direction == 'EGRESS':
        logging.error(
            'Kubernetes NetworkPolicy term %s no longer contains any destination '
            'addresses after the prefixes in destination_address_exclude were '
            'removed. Not rendering term.', self.term.name)
        self.term = None
        return

  def __str__(self):
    """Convert term to a string."""
    return yaml.safe_dump(self.ConvertToDict())

  def _validateDirection(self):
    if self.term.direction == 'INGRESS':
      if not self.term.source_address:
        raise K8sNetworkPolicyError(
            'Ingress rule missing required field "source-address"')

      if self.term.destination_address:
        raise K8sNetworkPolicyError('Ingress rules cannot include '
                                    '"destination-address.')

    elif self.term.direction == 'EGRESS':
      if self.term.source_address:
        raise K8sNetworkPolicyError(
            'Egress rules cannot include "source-address".')

      if not self.term.destination_address:
        raise K8sNetworkPolicyError(
            'Egress rule missing required field "destination-address".')

  def ConvertToDict(self):
    """Convert term to a dictionary.

    This is used to get a dictionary describing this term which can be
    output easily as a YAML object.

    Returns:
      A dictionary that contains a complete Kubernetes NetworkPolicy resource

    Raises:
      K8sNetworkPolicyError: The term name is not valid.
    """
    if not self.term:
      return {}

    if not self._TERM_NAME_RE.match(self.term.name):
      raise K8sNetworkPolicyError(
          'Term name %s is not valid. See https://kubernetes.io/docs/concepts/overview/working-with-objects/names for more information'
          % (self.term.name))

    resource_dict = {
        'apiVersion': self._API_VERSION,
        'kind': self._RESOURCE_KIND,
        'metadata': {
            'name': self.term.name,
            'annotations': {},
        },
        'spec': {
            'podSelector': {},
            'policyTypes': [self.term.direction.capitalize()]
        },
    }

    if self.term.comment:
      resource_dict['metadata']['annotations']['comment'] = ' '.join(
          self.term.comment)
    if self.term.owner:
      resource_dict['metadata']['annotations']['owner'] = self.term.owner

    # We only allow one kind of deny policy, and thats a default deny. Because
    # of that, we can quickly return an empty policy in the specified direction
    if 'deny' in self.term.action:
      return resource_dict

    peer_selectors = []
    peer_selector_key = ''
    base_port_selector = {}
    if self.term.direction == 'INGRESS':
      for source_address in self.term.source_address:
        peer_selector = {'ipBlock': {'cidr': str(source_address)}}
        peer_selectors.append(peer_selector)
      peer_selector_key = 'from'
    else:
      for destination_address in self.term.destination_address:
        peer_selector = {'ipBlock': {'cidr': str(destination_address)}}
        peer_selectors.append(peer_selector)
      peer_selector_key = 'to'

    # Build a base port selector list from ports
    base_port_selectors = []
    for start, end in self.term.destination_port:
      if start == end:
        base_port_selector = {'port': start}
      else:
        base_port_selector = {'port': start, 'endPort': end}

      base_port_selectors.append(base_port_selector)

    # Use the ports info to make one selector per port pair per proto
    port_selectors = []
    for proto in self.term.protocol:

      # If the list of ports is null, we still need to specify proto
      if not base_port_selectors:
        port_selectors.append({'protocol': proto.upper()})
        continue

      for base_selector in base_port_selectors:
        current_selector = copy.deepcopy(base_selector)
        # NetworkPolicies require uppercased proto name
        current_selector['protocol'] = proto.upper()
        port_selectors.append(current_selector)

    resource_dict['spec'][self.term.direction.lower()] = [{
        'ports': port_selectors,
        peer_selector_key: peer_selectors,
    }]

    return resource_dict


class K8s(aclgenerator.ACLGenerator):
  """A Kubernetes NetworkPolicy object."""

  _API_VERSION = 'networking.k8s.io/v1'
  _RESOURCE_KIND = 'NetworkPolicyList'
  _PLATFORM = 'k8s'
  SUFFIX = '.yml'
  _SUPPORTED_AF = frozenset(('mixed'))
  _GOOD_DIRECTION = ['INGRESS', 'EGRESS']
  _OPTIONAL_SUPPORTED_KEYWORDS = frozenset(['expiration'])

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, _ = super()._BuildTokens()

    # add extra things
    supported_tokens |= {'expiration', 'owner'}

    # remove unsupported things
    supported_tokens -= {'icmp_type', 'source-port', 'verbatim'}
    # easier to make a new structure
    supported_sub_tokens = {'action': {'accept', 'deny'}}

    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.network_policies = []
    total_rule_count = 0

    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)
      filter_name = header.FilterName(self._PLATFORM)

      direction = 'INGRESS'
      if filter_options:
        for i in self._GOOD_DIRECTION:
          if i in filter_options:
            direction = i
            filter_options.remove(i)

      term_names = set()
      for term in terms:
        if term.stateless_reply:
          logging.warning(
              'WARNING: Term %s in policy %s is a stateless reply '
              'term and will not be rendered.', term.name, filter_name)
          continue
        if not term.comment:
          term.comment = header.comment
        if direction == 'EGRESS':
          term.name += '-e'
        term.name = self.FixTermLength(term.name)
        if term.name in term_names:
          raise K8sNetworkPolicyError('Duplicate term name %s' % term.name)
        term_names.add(term.name)

        term.direction = direction
        if term.expiration:
          if term.expiration <= current_date:
            logging.warning(
                'WARNING: Term %s in policy %s is expired and '
                'will not be rendered.', term.name, filter_name)
            continue
          if term.expiration <= exp_info_date:
            logging.info(
                'INFO: Term %s in policy %s expires '
                'in less than two weeks.', term.name, filter_name)
        if term.option:
          raise K8sNetworkPolicyError(
              'Kubernetes NetworkPolicy does not support term options.')

        # Only generate the term if it's for the appropriate platform
        if term.platform:
          if self._PLATFORM not in term.platform:
            continue
        if term.platform_exclude:
          if self._PLATFORM in term.platform_exclude:
            continue

        term_dict = Term(term).ConvertToDict()
        if term_dict:
          total_rule_count += 1
          self.network_policies.append(term_dict)

    logging.info('Total rule count of policy %s is: %d', filter_name,
                 total_rule_count)

  def __str__(self):
    if not self.network_policies:
      return ''
    list_resource = {
        'apiVersion': self._API_VERSION,
        'kind': self._RESOURCE_KIND,
        'items': self.network_policies,
    }
    return yaml.safe_dump(list_resource)


# capirca/lib/nacaddr.py

# Copyright 2011 Google 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.
#

"""A subclass of the ipaddress library that includes comments for ipaddress."""

import collections
import ipaddress
import itertools
from typing import Union

import capirca.utils.iputils as iputils


def IP(ip, comment='', token='', strict=True):
  """Take an ip string and return an object of the correct type.

  Args:
    ip: the ip address.
    comment: option comment field
    token: option token name where this address was extracted from
    strict: If strict should be used in ipaddress object.

  Returns:
    ipaddress.IPv4 or ipaddress.IPv6 object or raises ValueError.

  Raises:
    ValueError: if the string passed isn't either a v4 or a v6 address.
  """
  if isinstance(ip, ipaddress._BaseNetwork):  # pylint disable=protected-access
    imprecise_ip = ip
  else:
    imprecise_ip = ipaddress.ip_network(ip, strict=strict)
  if imprecise_ip.version == 4:
    return IPv4(ip, comment, token, strict=strict)
  elif imprecise_ip.version == 6:
    return IPv6(ip, comment, token, strict=strict)
  raise ValueError('Provided IP string "%s" is not a valid v4 or v6 address'
                   % ip)


# TODO(robankeny) remove once at 3.7
@staticmethod
def _is_subnet_of(a, b):  # pylint: disable=invalid-name
  try:
    # Always false if one is v4 and the other is v6.
    if a.version != b.version:
      raise TypeError('%s and %s are not of the same version' % (a, b))
    return (b.network_address <= a.network_address and
            b.broadcast_address >= a.broadcast_address)
  except AttributeError:
    raise TypeError(
        'Unable to test subnet containment between %s and %s' % (a, b))


class IPv4(ipaddress.IPv4Network):
  """This subclass allows us to keep text comments related to each object."""

  def __init__(self, ip_string, comment='', token='', strict=True):
    self.text = comment
    self.token = token
    self.parent_token = token

    # Using a tuple of IP integer/prefixlength is significantly faster than
    # using the BaseNetwork object for recreating the IP network
    if isinstance(ip_string, ipaddress._BaseNetwork):  # pylint disable=protected-access
      ip = (ip_string.network_address._ip, ip_string.prefixlen)  # pylint disable=protected-access # pytype: disable=attribute-error
    else:
      ip = ip_string
    super().__init__(ip, strict)

  def subnet_of(self, other):
    """Return True if this network is a subnet of other."""
    if self.version != other.version:
      return False
    return self._is_subnet_of(self, other)

  def supernet_of(self, other):
    """Return True if this network is a supernet of other."""
    if self.version != other.version:
      return False
    return self._is_subnet_of(other, self)

  def __deepcopy__(self, memo):
    result = self.__class__(self)
    result.text = self.text
    result.token = self.token
    result.parent_token = self.parent_token
    return result

  def AddComment(self, comment=''):
    """Append comment to self.text, comma separated.

    Don't add the comment if it's the same as self.text.

    Args:
      comment: comment to be added.
    """
    if self.text:
      if comment and comment not in self.text:
        self.text += ', ' + comment
    else:
      self.text = comment

  def supernet(self, prefixlen_diff=1):
    """Override ipaddress.IPv4 supernet so we can maintain comments.

    See ipaddress.IPv4.Supernet for complete documentation.

    Args:
      prefixlen_diff: Prefix length difference.

    Returns:
      An IPv4 object

    Raises:
      PrefixlenDiffInvalidError: Raised when prefixlen - prefixlen_diff results
        in a negative number.
    """
    if self.prefixlen == 0:
      return self
    if self.prefixlen - prefixlen_diff < 0:
      raise PrefixlenDiffInvalidError(
          'current prefixlen is %d, cannot have a prefixlen_diff of %d' % (
              self.prefixlen, prefixlen_diff))
    ret_addr = IPv4(ipaddress.IPv4Network.supernet(self, prefixlen_diff),
                    comment=self.text, token=self.token)
    return ret_addr

  # Backwards compatibility name from v1.
  Supernet = supernet
  _is_subnet_of = _is_subnet_of


class IPv6(ipaddress.IPv6Network):
  """This subclass allows us to keep text comments related to each object."""

  def __init__(self, ip_string, comment='', token='', strict=True):
    self.text = comment
    self.token = token
    self.parent_token = token

    # Using a tuple of IP integer/prefixlength is significantly faster than
    # using the BaseNetwork object for recreating the IP network
    if isinstance(ip_string, ipaddress._BaseNetwork):  # pylint disable=protected-access
      ip = (ip_string.network_address._ip, ip_string.prefixlen)  # pylint disable=protected-access # pytype: disable=attribute-error
    else:
      ip = ip_string
    super().__init__(ip, strict)

  def subnet_of(self, other):
    """Return True if this network is a subnet of other."""
    if self.version != other.version:
      return False
    return self._is_subnet_of(self, other)

  def supernet_of(self, other):
    """Return True if this network is a supernet of other."""
    if self.version != other.version:
      return False
    return self._is_subnet_of(other, self)

  def __deepcopy__(self, memo):
    result = self.__class__(self)
    result.text = self.text
    result.token = self.token
    result.parent_token = self.parent_token
    return result

  def supernet(self, prefixlen_diff=1):
    """Override ipaddress.IPv6Network supernet so we can maintain comments.

    See ipaddress.IPv6Network.Supernet for complete documentation.
    Args:
      prefixlen_diff: Prefix length difference.

    Returns:
      An IPv4 object

    Raises:
      PrefixlenDiffInvalidError: Raised when prefixlen - prefixlen_diff results
        in a negative number.
    """
    if self.prefixlen == 0:
      return self
    if self.prefixlen - prefixlen_diff < 0:
      raise PrefixlenDiffInvalidError(
          'current prefixlen is %d, cannot have a prefixlen_diff of %d' % (
              self.prefixlen, prefixlen_diff))
    ret_addr = IPv6(ipaddress.IPv6Network.supernet(self, prefixlen_diff),
                    comment=self.text, token=self.token)
    return ret_addr

  # Backwards compatibility name from v1.
  Supernet = supernet
  _is_subnet_of = _is_subnet_of

  def AddComment(self, comment=''):
    """Append comment to self.text, comma separated.

    Don't add the comment if it's the same as self.text.

    Args:
      comment: comment to be added.
    """
    if self.text:
      if comment and comment not in self.text:
        self.text += ', ' + comment
    else:
      self.text = comment


IPType = Union[IPv4, IPv6]


def _InNetList(adders, ip):
  """Returns True if ip is contained in adders."""
  for addr in adders:
    if ip.subnet_of(addr):
      return True
  return False


def IsSuperNet(supernets, subnets):
  """Returns True if subnets are fully consumed by supernets."""
  for net in subnets:
    if not _InNetList(supernets, net):
      return False
  return True


def CollapseAddrListPreserveTokens(addresses):
  """Collapse an array of IPs only when their tokens are the same.

  Args:
     addresses: list of ipaddress.IPNetwork objects.

  Returns:
    list of ipaddress.IPNetwork objects.
  """
  ret_array = []
  for grp in itertools.groupby(sorted(addresses, key=lambda x: x.parent_token),
                               lambda x: x.parent_token):
    ret_array.append(CollapseAddrList(list(grp[1])))
  dedup_array = []
  i = 0
  while len(ret_array) > i:
    ip = ret_array.pop(0)
    k = 0
    to_add = True
    while k < len(dedup_array):
      if IsSuperNet(dedup_array[k], ip):
        to_add = False
        break
      elif IsSuperNet(ip, dedup_array[k]):
        del dedup_array[k]
      k += 1
    if to_add:
      dedup_array.append(ip)
  return [i for sublist in dedup_array for i in sublist]


def _SafeToMerge(address, merge_target, check_addresses):
  """Determine if it's safe to merge address into merge target.

  Checks given address against merge target and a list of check_addresses
  if it's OK to roll address into merge target such that it not less specific
  than any of the check_addresses. See description of why ir is important
  within public function CollapseAddrList.

  Args:
    address: Address that is being merged.
    merge_target: Merge candidate address.
    check_addresses: A dict networks_address->addrs to compare specificity with.

  Returns:
    True if safe to merge, False otherwise.
  """
  for check_address in check_addresses.get(address.network_address, []):
    if merge_target.netmask <= check_address.netmask < address.netmask:
      return False
  return True


def _CollapseAddrListInternal(addresses, complements_by_network):
  """Collapses consecutive netblocks until reaching a fixed point.

   Example:

   ip1 = ipaddress.IPv4Network('1.1.0.0/24')
   ip2 = ipaddress.IPv4Network('1.1.1.0/24')
   ip3 = ipaddress.IPv4Network('1.1.2.0/24')
   ip4 = ipaddress.IPv4Network('1.1.3.0/24')
   ip5 = ipaddress.IPv4Network('1.1.4.0/24')
   ip6 = ipaddress.IPv4Network('1.1.0.1/22')

   _CollapseAddrListInternal([ip1, ip2, ip3, ip4, ip5, ip6]) ->
   [IPv4Network('1.1.0.0/22'), IPv4Network('1.1.4.0/24')]

   Note, this shouldn't be called directly, but is called via
   CollapseAddrList([])

  Args:
    addresses: List of IPv4 or IPv6 objects
    complements_by_network: Dict of IPv4 or IPv6 objects indexed by
      network_address, that if present will be considered to avoid harmful
      optimizations.

  Returns:
    List of IPv4 or IPv6 objects (depending on what we were passed)
  """
  ret_array = []
  for addr in addresses:
    addr_is_fresh = True
    while addr_is_fresh:
      addr_is_fresh = False
      if not ret_array:
        ret_array.append(addr)
        continue

      prev_addr = ret_array[-1]
      if not _SafeToMerge(addr, prev_addr, complements_by_network):
        ret_array.append(addr)
      elif prev_addr.supernet_of(addr):
        # Preserve addr's comment, then subsume it.
        prev_addr.AddComment(addr.text)
      elif (prev_addr.version == addr.version and
            prev_addr.prefixlen == addr.prefixlen and
            # It's faster to compare integers than IP objects
            prev_addr.broadcast_address._ip + 1 == addr.network_address._ip and  # pylint disable=protected-access
            # Generating Supernet is relatively intensive compared to doing bit
            # operations
            (prev_addr.netmask._ip << 1) & prev_addr.network_address._ip ==      # pylint disable=protected-access
            prev_addr.network_address._ip):                                      # pylint disable=protected-access
        # Preserve addr's comment, then merge with it.
        prev_addr.AddComment(addr.text)
        addr = ret_array.pop().Supernet()
        addr_is_fresh = True
      else:
        ret_array.append(addr)

  return ret_array


def CollapseAddrList(addresses, complement_addresses=None):
  """Collapse an array of IP objects.

  Example:  CollapseAddrList(
    [IPv4('1.1.0.0/24'), IPv4('1.1.1.0/24')]) -> [IPv4('1.1.0.0/23')]
    Note: this works just as well with IPv6 addresses too.

  On platforms that support exclude semantics with most specific match,
  this method should _always_ be called with complement addresses supplied.
  Not doing so can lead to *reversal* of intent. Consider this case:

    destination-address:: 10.0.0.0/8, 10.0.0.0/10
    destination-exclude:: 10.0.0.0/9

  Without optimization, 10.0.0.1 will _match_. With optimization, most specific
  prefix will _not_ match, reversing the intent. Supplying complement_addresses
  allows this method to consider those implications.

  Args:
     addresses: list of ipaddress.IPNetwork objects
     complement_addresses: list of ipaddress.IPNetwork objects that, if present,
      will be considered to avoid harmful optimizations.

  Returns:
    list of ipaddress.IPNetwork objects
  """
  complements_dict = collections.defaultdict(list)
  address_set = set([a.network_address for a in addresses])
  for ca in complement_addresses or []:
    if ca.network_address in address_set:
      complements_dict[ca.network_address].append(ca)
  return _CollapseAddrListInternal(
      sorted(addresses, key=ipaddress.get_mixed_type_key), complements_dict)


def SortAddrList(addresses):
  """Return a sorted list of nacaddr objects."""
  return sorted(addresses, key=ipaddress.get_mixed_type_key)


def RemoveAddressFromList(superset, exclude):
  """Remove a single address from a list of addresses.

  Args:
    superset: a List of nacaddr IPv4 or IPv6 addresses
    exclude: a single nacaddr IPv4 or IPv6 address

  Returns:
    a List of nacaddr IPv4 or IPv6 addresses
  """
  ret_array = []
  for addr in superset:
    if exclude == addr or addr.subnet_of(exclude):
      pass
    elif exclude.version == addr.version and exclude.subnet_of(addr):
      # this could be optimized except that one group uses this
      # code with ipaddrs (instead of nacaddrs).
      ret_array.extend(IP(x) for x in iputils.exclude_address(addr, exclude))
    else:
      ret_array.append(addr)
  return SortAddrList(ret_array)


def AddressListExclude(superset, excludes, collapse_addrs=True):
  """Remove a list of addresses from another list of addresses.

  Args:
    superset: a List of nacaddr IPv4 or IPv6 addresses
    excludes: a List nacaddr IPv4 or IPv6 addresses
    collapse_addrs: whether or not to collapse contiguous CIDRs togethe

  Returns:
    a List of nacaddr IPv4 or IPv6 addresses
  """
  if collapse_addrs:
    superset = CollapseAddrList(superset)[::-1]
    excludes = CollapseAddrList(excludes)[::-1]
  else:
    superset = sorted(superset, reverse=True)
    excludes = sorted(excludes, reverse=True)

  ret_array = []
  while superset and excludes:
    if superset[-1].overlaps(excludes[-1]):
      ip = superset.pop()
      superset.extend(
          reversed(RemoveAddressFromList([ip], excludes[-1])))
    elif superset[-1]._get_networks_key() < excludes[-1]._get_networks_key():  # pylint: disable=protected-access
      ret_array.append(superset.pop())
    else:
      excludes.pop()
  if collapse_addrs:
    return CollapseAddrList(ret_array + superset)
  else:
    return sorted(set(ret_array + superset))


ExcludeAddrs = AddressListExclude


class PrefixlenDiffInvalidError(ipaddress.NetmaskValueError):
  """Holdover from ipaddr v1."""


if __name__ == '__main__':
  pass


# capirca/lib/naming.py

# Copyright 2011 Google 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.
#

"""Parse naming definition files.

Network access control applications use definition files which contain
information about networks and services.  This naming class
will provide an easy interface into using these definitions.

Sample usage with definition files contained in ./acl/defs:
    defs = Naming('acl/defs/')

    services =  defs.GetService('DNS')
      returns ['53/tcp', '53/udp', ...]

    networks = defs.GetNet('INTERNAL')
      returns a list of nacaddr.IPv4 object

The definition files are contained in a single directory and
may consist of multiple files ending in .net or .svc extensions,
indicating network or service definitions respectively.  The
format of the files consists of a 'token' value, followed by a
list of values and optional comments, such as:

INTERNAL = 10.0.0.0/8     # RFC-1918
           172.16.0.0/12  # RFC-1918
           192.168.0.0/16 # RFC-1918
or

DNS = 53/tcp
      53/udp

"""

import glob
import os
import re

from absl import logging

from capirca.lib import nacaddr
from capirca.lib import port as portlib


class Error(Exception):
  """Create our own base error class to be inherited by other error classes."""


class NamespaceCollisionError(Error):
  """Used to report on duplicate symbol names found while parsing."""


class BadNetmaskTypeError(Error):
  """Used to report on duplicate symbol names found while parsing."""


class NoDefinitionsError(Error):
  """Raised if no definitions are found."""


class ParseError(Error):
  """Raised if an error occurs during parsing."""


class UndefinedAddressError(Error):
  """Raised if an address is referenced but not defined."""


class UndefinedServiceError(Error):
  """Raised if a service is referenced but not defined."""


class UndefinedPortError(Error):
  """Raised if a port/protocol pair has not been defined."""


class UnexpectedDefinitionTypeError(Error):
  """An unexpected/unknown definition type was used."""


class NamingSyntaxError(Error):
  """A general syntax error for the definition."""


class _ItemUnit:
  """This class is a container for an index key and a list of associated values.

  An ItemUnit will contain the name of either a service or network group,
  and a list of the associated values separated by spaces.

  Attributes:
    name: A string representing a unique token value.
    items: a list of strings containing values for the token.
  """

  def __init__(self, symbol):
    self.name = symbol
    self.items = []


class Naming:
  """Object to hold naming objects from NETWORK and SERVICES definition files.

  Attributes:
     current_symbol: The current token being handled while parsing data.
     services: A collection of all of the current service item tokens.
     networks: A collection of all the current network item tokens.
     unseen_services: Undefined service entries.
     unseen_networks: Undefined network entries.
     port_re: Regular Expression matching valid port entries.
  """

  def __init__(self, naming_dir=None, naming_file=None, naming_type=None):
    """Set the default values for a new Naming object."""
    self.current_symbol = None
    self.services = {}
    self.networks = {}
    self.unseen_services = {}
    self.unseen_networks = {}
    self.port_re = re.compile(r'(^\d+-\d+|^\d+)\/\w+$|^[\w\d-]+$',
                              re.IGNORECASE | re.DOTALL)
    self.token_re = re.compile(r'(^[-_A-Z0-9]+$)', re.IGNORECASE)
    if naming_file and naming_type:
      filename = os.path.sep.join([naming_dir, naming_file])
      with open(filename, 'r') as file_handle:
        self._ParseFile(file_handle, naming_type)
    elif naming_dir:
      self._Parse(naming_dir, 'services')
      self._CheckUnseen('services')

      self._Parse(naming_dir, 'networks')
      self._CheckUnseen('networks')

  def _CheckUnseen(self, def_type):
    if def_type == 'services':
      if self.unseen_services:
        raise UndefinedServiceError('%s %s' % (
            'The following tokens were nested as a values, but not defined',
            self.unseen_services))
    if def_type == 'networks':
      if self.unseen_networks:
        raise UndefinedAddressError('%s %s' % (
            'The following tokens were nested as a values, but not defined',
            self.unseen_networks))

  def GetIpParents(self, query):
    """Return network tokens that contain IP in query.

    Args:
      query: an ip string ('10.1.1.1') or nacaddr.IP object

    Returns:
      A sorted list of unique parent tokens.
    """
    base_parents = []
    recursive_parents = []
    # convert string to nacaddr, if arg is ipaddr then convert str() to nacaddr
    if (not isinstance(query, nacaddr.IPv4) and
       not isinstance(query, nacaddr.IPv6)):
      if query[:1].isdigit():
        query = nacaddr.IP(query)
    # Get parent token for an IP
    if isinstance(query, nacaddr.IPv4) or isinstance(query, nacaddr.IPv6):
      for token in self.networks:
        for item in self.networks[token].items:
          item = item.split('#')[0].strip()
          if not item[:1].isdigit():
            continue
          try:
            supernet = nacaddr.IP(item, strict=False)
            if supernet.supernet_of(query):
              base_parents.append(token)
          except ValueError:
            # item was not an IP
            pass
    # Get parent token for another token
    else:
      for token in self.networks:
        for item in self.networks[token].items:
          item = item.split('#')[0].strip()
          if item[:1].isalpha() and item == query:
            base_parents.append(token)
    # look for nested tokens
    for bp in base_parents:
      done = False
      for token in self.networks:
        if bp in [item.split('#')[0].strip() for item in
                  self.networks[token].items]:
          # ignore IPs, only look at token values
          if bp[:1].isalpha():
            if bp not in recursive_parents:
              recursive_parents.append(bp)
              recursive_parents.extend(self.GetIpParents(bp))
            done = True
      # if no nested tokens, just append value
      if not done:
        if bp[:1].isalpha() and bp not in recursive_parents:
          recursive_parents.append(bp)
    return sorted(list(set(recursive_parents)))

  def GetServiceParents(self, query):
    """Given a query token, return list of services definitions with that token.

    Args:
      query: a service token name.
    Returns:
      List of service definitions containing the token.
    """
    return self._GetParents(query, self.services)

  def GetNetParents(self, query):
    """Given a query token, return list of network definitions with that token.

    Args:
      query: a network token name.
    Returns:
      A list of network definitions containing the token.
    """
    return self._GetParents(query, self.networks)

  def _GetParents(self, query, query_group):
    """Given a naming item dict, return any tokens containing the value.

    Args:
      query: a service or token name, such as 53/tcp or DNS
      query_group: either services or networks dict

    Returns:
      Returns a list of definitions containing the token in desired group.
    """
    base_parents = []
    recursive_parents = []
    # collect list of tokens containing query
    for token in query_group:
      if query in [item.split('#')[0].strip() for item in
                   query_group[token].items]:
        base_parents.append(token)
    if not base_parents:
      return []
    # iterate through tokens containing query, doing recursion if necessary
    for bp in base_parents:
      for token in query_group:
        if bp in query_group[token].items and bp not in recursive_parents:
          recursive_parents.append(bp)
          recursive_parents.extend(self._GetParents(bp, query_group))
      if bp not in recursive_parents:
        recursive_parents.append(bp)
    return recursive_parents

  def GetNetChildren(self, query):
    """Given a query token, return list of network definitions tokens within provided token.

    This will only return children, not descendants of provided token.

    Args:
      query: a network token name.

    Returns:
      A list of network definitions tokens within this token.
    """
    return self._GetChildren(query, self.networks)

  def _GetChildren(self, query, query_group):
    """Given a naming item dict, return tokens (not IPs) contained within this value.

    Args:
      query: a token name
      query_group: networks dict

    Returns:
      Returns a list of definitions tokens within (children) target token.
    """

    children = []
    if query in query_group:
      for item in query_group[query].items:
        child = item.split('#')[0].strip()

        # Determine if item a token, then it's a child
        if not self._IsIpFormat(child):
          children.append(child)

    return children

  def _IsIpFormat(self, item):
    """Helper function for _GetChildren to detect if string is IP format.

    Args:
      item: string either a IP or token.
    Returns:
      True if string is a IP
      False if string is not a IP
    """
    try:
      item = item.strip()
      nacaddr.IP(item, strict=False)
      return True
    except ValueError:
      return False

  def GetServiceNames(self):
    """Returns the list of all known service names."""
    return list(self.services.keys())

  def GetService(self, query):
    """Given a service name, return a list of associated ports and protocols.

    Args:
      query: Service name symbol or token.

    Returns:
      A list of service values such as ['80/tcp', '443/tcp', '161/udp', ...]

    Raises:
      UndefinedServiceError: If the service name isn't defined.
    """
    expandset = set()
    already_done = set()
    data = []
    service_name = ''
    data = query.split('#')     # Get the token keyword and remove any comment
    service_name = data[0].split()[0]  # strip and cast from list to string
    if service_name not in self.services:
      raise UndefinedServiceError('\nNo such service: %s' % query)

    already_done.add(service_name)

    for next_item in self.services[service_name].items:
      # Remove any trailing comment.
      service = next_item.split('#')[0].strip()
      # Recognized token, not a value.
      if '/' not in service:
        # Make sure we are not descending into recursion hell.
        if service not in already_done:
          already_done.add(service)
          try:
            expandset.update(self.GetService(service))
          except UndefinedServiceError as e:
            # One of the services in query is undefined, refine the error msg.
            raise UndefinedServiceError('%s (in %s)' % (e, query))
      else:
        expandset.add(service)
    return sorted(expandset)

  def GetPortParents(self, query, proto):
    """Returns a list of all service tokens containing the port/protocol pair.

    Args:
        query: port number ('22') as str
        proto: protocol name ('tcp') as str

    Returns:
        A list of service tokens: ['SSH', 'HTTPS']

    Raises:
      UndefinedPortError: If the port/protocol pair isn't used in any
      service tokens.
    """
    # turn the given port and protocol into a PortProtocolPair object
    given_ppp = portlib.PPP(query + '/' + proto)
    base_parents = []
    matches = set()
    # check each service token to see if it's a PPP or a nested group.
    # if it's a PPP, see if there's a match with given_ppp
    # otherwise, add nested group to a list to recurisvely check later.
    # if there's no match, do nothing.
    for service_token in self.services:
      for port_child in self.services[service_token].items:
        ppp = portlib.PPP(port_child)
        # check for exact match
        if ppp.is_single_port and ppp == given_ppp:
          matches.add(service_token)
        # check if it's within ppp's port range
        elif ppp.is_range and given_ppp in ppp:
          matches.add(service_token)
        # if it's a nested token, add to a list to recurisvely
        # check later.
        elif ppp.nested:
          if service_token not in base_parents:
            base_parents.append(service_token)
    # break down the nested service tokens into PPP objects and check
    # against given_ppp
    for bp in base_parents:
      for port_child in self.GetService(bp):
        ppp = portlib.PPP(port_child)
        # check for exact match
        if ppp.is_single_port and ppp == given_ppp:
          matches.add(bp)
        # check if it's within ppp's port range
        elif ppp.is_range and given_ppp in ppp:
          matches.add(bp)
    # error if the port/protocol pair is not found.
    if not matches:
      raise UndefinedPortError(
          '%s/%s is not found in any service tokens' % (query, proto))
    return sorted(matches)

  def GetServiceByProto(self, query, proto):
    """Given a service name, return list of ports in the service by protocol.

    Args:
      query: Service name to lookup.
      proto: A particular protocol to restrict results by, such as 'tcp'.

    Returns:
      A list of service values of type 'proto', such as ['80', '443', ...]

    Raises:
      UndefinedServiceError: If the service name isn't defined.
    """
    services_set = set()
    proto = proto.upper()
    data = []
    servicename = ''
    data = query.split('#')     # Get the token keyword and remove any comment
    servicename = data[0].split()[0]  # strip and cast from list to string
    if servicename not in self.services:
      raise UndefinedServiceError('%s %s' % ('\nNo such service,', servicename))

    for service in self.GetService(servicename):
      if service and '/' in service:
        parts = service.split('/')
        if parts[1].upper() == proto:
          services_set.add(parts[0])
    return sorted(services_set)

  def GetNetAddr(self, token):
    """Given a network token, return a list of nacaddr.IPv4 or nacaddr.IPv6 objects.

    Args:
      token: A name of a network definition, such as 'INTERNAL'

    Returns:
      A list of nacaddr.IPv4 or nacaddr.IPv6 objects.

    Raises:
      UndefinedAddressError: if the network name isn't defined.
    """
    return self.GetNet(token)

  def GetNet(self, query):
    """Expand a network token into a list of nacaddr.IPv4 or nacaddr.IPv6 objects.

    Args:
      query: Network definition token which may include comment text

    Raises:
      BadNetmaskTypeError: Results when an unknown netmask_type is
      specified.  Acceptable values are 'cidr', 'netmask', and 'hostmask'.

    Returns:
      List of nacaddr.IPv4 or nacaddr.IPv6 objects

    Raises:
      UndefinedAddressError: for an undefined token value
    """
    returnlist = []
    data = []
    token = ''
    data = query.split('#')     # Get the token keyword and remove any comment
    token = data[0].split()[0]  # Remove whitespace and cast from list to string
    if token not in self.networks:
      raise UndefinedAddressError('%s %s' % ('\nUNDEFINED:', str(token)))

    for i in self.networks[token].items:
      comment = ''
      if i.find('#') > -1:
        (net, comment) = i.split('#', 1)
      else:
        net = i

      net = net.strip()
      if self.token_re.match(net):
        returnlist.extend(self.GetNet(net))
      else:
        try:
          # TODO(robankeny): Fix using error to continue processing.
          addr = nacaddr.IP(net, strict=False)
          addr.text = comment.lstrip()
          addr.token = token
          returnlist.append(addr)
        except ValueError:
          # if net was something like 'FOO', or the name of another token which
          # needs to be dereferenced, nacaddr.IP() will return a ValueError
          returnlist.extend(self.GetNet(net))
    for i in returnlist:
      i.parent_token = token
    return returnlist

  def _Parse(self, defdirectory, def_type):
    """Parse files of a particular type for tokens and values.

    Given a directory name and the type (services|networks) to
    process, grab all the appropriate files in that directory
    and parse them for definitions.

    Args:
      defdirectory: Path to directory containing definition files.
      def_type: Type of definitions to parse

    Raises:
      NoDefinitionsError: if no definitions are found.
    """
    file_names = []
    get_files = {'services': lambda: glob.glob(defdirectory + '/*.svc'),
                 'networks': lambda: glob.glob(defdirectory + '/*.net')}

    if def_type in get_files:
      file_names = get_files[def_type]()
    else:
      raise NoDefinitionsError('Definitions type %s is unknown.' % def_type)
    if not file_names:
      raise NoDefinitionsError('No definition files for %s in %s found.' %
                               (def_type, defdirectory))

    for current_file in file_names:
      try:
        with open(current_file, 'r') as file_handle:
          self._ParseFile(file_handle, def_type)
      except IOError as error_info:
        raise NoDefinitionsError('%s' % error_info)

  def _ParseFile(self, file_handle, def_type):
    for line in file_handle:
      self._ParseLine(line, def_type)

  def ParseServiceList(self, data):
    """Take an array of service data and import into class.

    This method allows us to pass an array of data that contains service
    definitions that are appended to any definitions read from files.

    Args:
      data: array of text lines containing service definitions.
    """
    for line in data:
      self._ParseLine(line, 'services')

  def ParseNetworkList(self, data):
    """Take an array of network data and import into class.

    This method allows us to pass an array of data that contains network
    definitions that are appended to any definitions read from files.

    Args:
      data: array of text lines containing net definitions.

    """
    for line in data:
      self._ParseLine(line, 'networks')

  def _ParseLine(self, line, definition_type):
    """Parse a single line of a service definition file.

    This routine is used to parse a single line of a service
    definition file, building a list of 'self.services' objects
    as each line of the file is iterated through.

    Args:
      line: A single line from a service definition files.
      definition_type: Either 'networks' or 'services'

    Raises:
      UnexpectedDefinitionTypeError: called with unexpected type of definitions.
      NamespaceCollisionError: when overlapping tokens are found.
      ParseError: If errors occur
      NamingSyntaxError: Syntax error parsing config.
    """
    if definition_type not in ['services', 'networks']:
      raise UnexpectedDefinitionTypeError('%s %s' % (
          'Received an unexpected definition type:', definition_type))
    line = line.strip()
    if not line or line.startswith('#'):  # Skip comments and blanks.
      return
    comment = ''
    if line.find('#') > -1:  # if there is a comment, save it
      (line, comment) = line.split('#', 1)
    line_parts = line.split('=')   # Split on var = val lines.
    # the value field still has the comment at this point
    # If there was '=', then do var and value
    if len(line_parts) > 1:
      current_symbol = line_parts[0].strip()  # varname left of '='
      if not self.token_re.match(current_symbol):
        logging.info('\nService name does not match recommended criteria: %s\nOnly A-Z, a-z, 0-9, -, and _ allowed' % current_symbol)
      self.current_symbol = current_symbol
      if definition_type == 'services':
        for port in line_parts[1].strip().split():
          if not self.port_re.match(port):
            raise NamingSyntaxError('%s: %s' % (
                'The following line has a syntax error', line))
        if self.current_symbol in self.services:
          raise NamespaceCollisionError('%s %s' % (
              '\nMultiple definitions found for service: ',
              self.current_symbol))
      elif definition_type == 'networks':
        if self.current_symbol in self.networks:
          raise NamespaceCollisionError('%s %s' % (
              '\nMultiple definitions found for service: ',
              self.current_symbol))

      self.unit = _ItemUnit(self.current_symbol)
      if definition_type == 'services':
        self.services[self.current_symbol] = self.unit
        # unseen_services is a list of service TOKENS found in the values
        # of newly defined services, but not previously defined themselves.
        # When we define a new service, we should remove it (if it exists)
        # from the list of unseen_services.
        if self.current_symbol in self.unseen_services:
          self.unseen_services.pop(self.current_symbol)
      elif definition_type == 'networks':
        self.networks[self.current_symbol] = self.unit
        if self.current_symbol in self.unseen_networks:
          self.unseen_networks.pop(self.current_symbol)
      else:
        raise ParseError('Unknown definitions type.')
      values = line_parts[1]
    # No '=', so this is a value only line
    else:
      values = line_parts[0]  # values for previous var are continued this line
    for value_piece in values.split():
      if not value_piece:
        continue
      if not self.current_symbol:
        break
      if comment:
        self.unit.items.append(value_piece + ' # ' + comment)
      else:
        self.unit.items.append(value_piece)
        # token?
        if value_piece[0].isalpha() and ':' not in value_piece:
          if definition_type == 'services':
            # already in top definitions list?
            if value_piece not in self.services:
              # already have it as an unused value?
              if value_piece not in self.unseen_services:
                self.unseen_services[value_piece] = True
          if definition_type == 'networks':
            if value_piece not in self.networks:
              if value_piece not in self.unseen_networks:
                self.unseen_networks[value_piece] = True


# capirca/lib/nftables.py

# Copyright 2014 Google 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.

"""nftables generator.

nftables is the new packet classification framework that intends to replace
the existing {ip,ip6,arp,eb}_tables infrastructure.

nftables syntax is radically different from iptables and therefore this
generator inherits directly from aclgenerator.

"""

import collections
import datetime

from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import nacaddr
import six


class Error(Exception):
  """Base error class."""


class InvalidTargetOption(Error):
  """Raised when target specification is invalid."""


class InvalidAddressFamily(Error):
  """Raised when address family specification is invalid."""


class Term(aclgenerator.Term):
  """Representation of an individual nftables term.

  This is mostly useful for the __str__() method.

  """

  _PLATFORM = 'nftables'
  _ACTIONS = {'accept': 'accept',
              'deny': 'drop',
              'reject': 'reject',
              'next': 'continue',
              'reject-with-tcp-rst': 'reject with tcp reset'}
  MAX_CHARACTERS = 128

  def __init__(self, term, af):
    """Setup a new nftables term.

    Args:
      term: A policy.Term object
      af: The capirca address family for the term, "inet", "inet6", or "mixed"

    Raises:
      InvalidAddressFamily: if supplied target options are invalid.

    Note: AF of mixed requires kernel 3.15 or higher
    """
    super().__init__(term)
    self.term = term
    self.af = af
    if af == 'inet6':
      self.all_ips = nacaddr.IPv6('::/0')
    elif af == 'inet':
      self.all_ips = nacaddr.IPv4('0.0.0.0/0')
    elif af == 'mixed':
      # TODO(castagno): Need to add support for a mixed address family
      raise InvalidAddressFamily('Address family mixed is not supported yet')
    else:
      raise InvalidAddressFamily('Not a valid address family')

  # TODO(vklimovs): some not trivial processing is happening inside this
  # __str__, replace with explicit method
  def __str__(self):
    output = []

    # Don't render term if not in platforms or in excluded platforms
    if self.term.platform and self._PLATFORM not in self.term.platform:
      return ''
    if (self.term.platform_exclude and
        self._PLATFORM in self.term.platform_exclude):
      return ''

    # Don't render icmpv6 protocol terms under inet, or icmp under inet6
    # Does not currently support mixed family.
    if ((self.af == 'inet6' and 'icmp' in self.term.protocol) or
        (self.af == 'inet' and 'icmpv6' in self.term.protocol)):
      logging.debug(self.NO_AF_LOG_PROTO.substitute(
          term=self.term.name,
          proto=', '.join(self.term.protocol),
          af=self.af))
      return ''

    # Term verbatim output - this will skip over most normal term
    # creation code by returning early. Warnings provided in policy.py.
    if self.term.verbatim:
      for verbatim_line in self.term.verbatim:
        platform, contents = verbatim_line
        if platform == self._PLATFORM:
          output.append(str(contents))
      return '\n'.join(output)

    # Source address
    if self.term.source_address or self.term.source_address_exclude:
      src_addrs = self._CalculateAddrs(self.term.source_address,
                                       self.term.source_address_exclude)
      if not src_addrs:
        logging.warning(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                       direction='source',
                                                       af=self.af))
        return ''
      # TODO(castagno): Add support for ipv6
      output.append('ip saddr %s' % self._FormatMatch(src_addrs))

    # Destination address
    if self.term.destination_address or self.term.source_address_exclude:
      dst_addrs = self._CalculateAddrs(self.term.destination_address,
                                       self.term.destination_address_exclude)
      if not dst_addrs:
        logging.warning(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                       direction='destination',
                                                       af=self.af))
        return ''
      # TODO(castagno): Add support for ipv6
      output.append('ip daddr %s' % self._FormatMatch(dst_addrs))

    # Protocol
    #
    # nft intepreter shortcuts protocol specification if there are more specific
    # matches. At the moment, these are:
    # * source port
    # * destination port
    # * ICMP type
    if self.term.protocol and not (self.term.source_port or
                                   self.term.destination_port or
                                   self.term.icmp_type):
      output.append('ip protocol %s' % self._FormatMatch(self.term.protocol))

    # Source port
    if self.term.source_port:
      output.append('%s sport %s' %
                    (self._FormatMatch(self.term.protocol),
                     self._FormatMatch(self.term.source_port)))

    # Destination port
    if self.term.destination_port:
      output.append('%s dport %s' %
                    (self._FormatMatch(self.term.protocol),
                     self._FormatMatch(self.term.destination_port)))

    # Icmp type
    if self.term.icmp_type:
      icmp_types = self.NormalizeIcmpTypes(self.term.icmp_type,
                                           self.term.protocol,
                                           self.af)
      if icmp_types != ['']:
        # nft intepreter requires ICMP types to be spelled out
        icmp_name_types = self.ICMP_TYPE[self.AF_MAP[self.af]]
        icmp_type_names = dict((v, k) for k, v in icmp_name_types.items())
        output.append('icmp type %s' %
                      self._FormatMatch([icmp_type_names[icmp_type] for
                                         icmp_type in icmp_types]))
    # Counter
    # This does not use the value that was passed in the term.
    if self.term.counter:
      output.append('counter')

    # Log
    # Setup logic so that only one log statement is printed.
    if self.term.logging and not self.term.log_name:
      output.append('log')
    elif (self.term.logging and self.term.log_name) or self.term.log_name:
      # Only supports log prefix's of 128 characters truncate to 126 to support
      # the additional suffix that is being added
      output.append('log prefix "%s: "' % self.term.log_name[:126])

    # Action
    output.append(self._ACTIONS[self.term.action[0]])

    # Owner (implement as comment)
    if self.term.owner:
      self.term.comment.append('Owner: %s' % self.term.owner)

    # Comment
    if self.term.comment:
      comment_data = ' '.join(self.term.comment)
      # Have to truncate MAX_CHARACTERS characters due to NFTables limitation
      if len(comment_data) > self.MAX_CHARACTERS:
        # Have to use the first MAX_CHARACTERS characters
        comment_data = comment_data[:self.MAX_CHARACTERS]
        logging.warning(
            'Term %s in policy is too long (>%d characters) '
            'and will be truncated', self.term.name, self.MAX_CHARACTERS)

      output.append('comment "%s"' % comment_data)

    return ' '.join(output)

  def _CalculateAddrs(self, addr_list, addr_exclude_list):
    addr_list = [addr for addr in addr_list
                 if addr.version == self.AF_MAP[self.af]]
    if addr_exclude_list:
      if not addr_list:
        addr_list = [self.all_ips]
      addr_list = nacaddr.ExcludeAddrs(addr_list, addr_exclude_list)
    return addr_list

  def _FormatMatch(self, match):
    output = []
    for element in match:
      # Special case: port range
      if isinstance(element, tuple):
        range_start, range_end = element
        if range_start == range_end:
          output.append('%d' % range_start)
        else:
          output.append('%d-%d' % (range_start, range_end))
      else:
        output.append(str(element))
    if len(output) > 1:
      # idiosyncrasy of nftables output: no leading space to trailing }
      return '{ ' + ', '.join(output) + '}'
    else:
      return output[0]


class Nftables(aclgenerator.ACLGenerator):
  """nftables generator.

    This class takes a policy object and renders the output into a syntax
    which is nft intepreter.
  """

  SUFFIX = '.nft'
  _PLATFORM = 'nftables'
  _TERM = Term
  # https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes#Tables
  _VALID_ADDRESS_FAMILIES = {'inet': 'ip', 'inet6': 'ip6', 'mixed': 'inet'}
  # https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks
  _VALID_HOOK_NAMES = set(['prerouting', 'input', 'forward',
                           'output', 'postrouting'])

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'owner', 'counter', 'logging', 'log_name'}
    del supported_sub_tokens['option']
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, policy, expiration):
    """Translates policy contents to platform specific data structures.

    Args:
      policy: policy object to be transalted to platform specific data
        structures.
      expiration: integer number of weeks to be warned about term expiration in.

    Raises:
      InvalidTargetOption: if supplied target options are invalid.

    """
    self.tables = collections.defaultdict(list)

    for header, terms in policy.filters:
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)

      if not filter_options:
        raise InvalidTargetOption('Chain name not specified.')

      if len(filter_options) > 4:
        raise InvalidTargetOption('Too many target options.')

      if len(filter_options) == 1:
        raise InvalidTargetOption(
            'Must have at least hook name')

      # Chain name, mandatory
      chain_name = filter_options[0]

      # Hook name, mandatory
      hook_name = filter_options[1].lower()

      if hook_name not in self._VALID_HOOK_NAMES:
        raise InvalidTargetOption(
            'Specified hook name (%s) is not a valid hook name.' % hook_name)

      # chain priority, mandatory
      chain_priority = None
      if len(filter_options) >= 3:
        try:
          chain_priority = str(int(filter_options[2]))
        except ValueError:
          raise InvalidTargetOption(
              'Specified chain priority is not an integer (%s).'
              % filter_options[2])

      # TODO(castagno): fix this. If you dont have hook name it never prints
      # anyways, so its not really optional
      if not hook_name or not chain_priority:
        logging.info('Chain %s is a non-base chain, make sure it is linked.',
                     chain_name)
        raise InvalidTargetOption('A table name is required')

      # Address family, optional, defaults to capirca inet
      af = 'inet'
      if len(filter_options) == 4:
        af = filter_options[3]
        if af not in self._VALID_ADDRESS_FAMILIES:
          raise InvalidTargetOption(
              'Specified address family (%s) is not supported.' % af)

      # Terms
      valid_terms = []
      for term in terms:
        term = self.FixHighPorts(term, af)
        if not term:
          continue

        current_date = datetime.datetime.utcnow().date()
        expiration_date = current_date + datetime.timedelta(weeks=expiration)

        if term.expiration:
          if term.expiration < current_date:
            logging.warning(
                'Term %s in policy %s is expired and will not be rendered.',
                term.name, chain_name)
            continue
          if term.expiration <= expiration_date:
            logging.info('Term %s in policy %s expires in less than %d weeks.',
                         term.name, chain_name, expiration)

        valid_terms.append(self._TERM(term, af))

      # Add to appropriate table
      self.tables[af].append((chain_name, hook_name,
                              chain_priority, valid_terms))

  # TODO(vklimovs): some not trivial processing is happening inside this
  # __str__, replace with explicit method
  def __str__(self):
    output = []

    # Iterate over tables
    for af in sorted(self.tables):
      output.append('flush table %s table_filter' %
                    self._VALID_ADDRESS_FAMILIES[af])
      output.append('table %s table_filter {' %
                    self._VALID_ADDRESS_FAMILIES[af])

      # Iterate over chains
      for (chain_name, hook_name,
           chain_priority, valid_terms) in self.tables[af]:
        output.append('\tchain %s {' % chain_name)
        if hook_name and chain_priority:
          output.append('\t\ttype filter hook %s priority %s;' %
                        (hook_name, chain_priority))
          for valid_term in valid_terms:
            term_string = str(valid_term)
            if term_string:
              output.append('\t\t' + term_string)
        output.append('\t}')  # chain
      output.append('}')  # table
    return '\n'.join(output)


# capirca/lib/nsxv.py

# Copyright 2015 The Capirca Project Authors 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.
#

"""Nsxv generator."""

import datetime
import re
import xml

from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import nacaddr
import six


_ACTION_TABLE = {
    'accept': 'allow',
    'deny': 'deny',
    'reject': 'reject',
    'reject-with-tcp-rst': 'reject',  # tcp rst not supported
}

_XML_TABLE = {
    'actionStart': '<action>',
    'actionEnd': '</action>',
    'srcIpv4Start': '<source><type>Ipv4Address</type><value>',
    'srcIpv4End': '</value></source>',
    'destIpv4Start': '<destination><type>Ipv4Address</type><value>',
    'destIpv4End': '</value></destination>',
    'protocolStart': '<protocol>',
    'protocolEnd': '</protocol>',
    'serviceStart': '<service>',
    'serviceEnd': '</service>',
    'appliedToStart': '<appliedTo><type>SecurityGroup</type><value>',
    'appliedToEnd': '</value></appliedTo>',
    'srcPortStart': '<sourcePort>',
    'srcPortEnd': '</sourcePort>',
    'destPortStart': '<destinationPort>',
    'destPortEnd': '</destinationPort>',
    'icmpTypeStart': '<subProtocol>',
    'icmpTypeEnd': '</subProtocol>',
    'logTrue': '<loggingEnabled>true</loggingEnabled>',
    'logFalse': '<loggingEnabled>false</loggingEnabled>',
    'sectionStart': '<section>',
    'sectionEnd': '</section>',
    'nameStart': '<name>',
    'nameEnd': '</name>',
    'srcIpv6Start': '<source><type>Ipv6Address</type><value>',
    'srcIpv6End': '</value></source>',
    'destIpv6Start': '<destination><type>Ipv6Address</type><value>',
    'destIpv6End': '</value></destination>',
    'noteStart': '<notes>',
    'noteEnd': '</notes>',
}

_NSXV_SUPPORTED_KEYWORDS = [
    'name',
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'expiration',
    'logging'
]


# generic error class
class Error(Exception):
  """Generic error class."""
  pass


class UnsupportedNsxvAccessListError(Error):
  """Raised when we're give a non named access list."""
  pass


class NsxvAclTermError(Error):
  """Raised when there is a problem in a nsxv access list."""
  pass


class NsxvDuplicateTermError(Error):
  """Raised when there is a duplicate."""
  pass


class Term(aclgenerator.Term):
  """Creates a  single ACL Term for Nsxv."""

  def __init__(self, term, filter_type, applied_to=None, af=4):
    self.term = term
    # Our caller should have already verified the address family.
    assert af in (4, 6)
    self.af = af
    self.filter_type = filter_type
    self.applied_to = applied_to

  def __str__(self):
    """Convert term to a rule string.

    Returns:
      A rule as a string.

    Raises:
      NsxvAclTermError: When unknown icmp-types are specified

    """
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if 'nsxv' not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if 'nsxv' in self.term.platform_exclude:
        return ''

    ret_str = ['']

    # Don't render icmpv6 protocol terms under inet, or icmp under inet6
    if ((self.af == 6 and 'icmp' in self.term.protocol) or
        (self.af == 4 and 'icmpv6' in self.term.protocol)):
      logging.debug(self.NO_AF_LOG_PROTO.substitute(term=self.term.name,
                                                    proto=self.term.protocol,
                                                    af=self.filter_type))
      return ''

    # Term verbatim is not supported
    if self.term.verbatim:
      raise NsxvAclTermError(
          'Verbatim are not implemented in standard ACLs')

    # Term option is not supported
    if self.term.option:
      for opt in [str(single_option) for single_option in self.term.option]:
        if((opt.find('tcp-established') == 0)
           or (opt.find('established') == 0)):
          return ''
        else:
          raise NsxvAclTermError(
              'Option are not implemented in standard ACLs')

    # check for keywords Nsxv does not support
    term_keywords = self.term.__dict__
    unsupported_keywords = []
    for key  in term_keywords:
      if term_keywords[key]:
        # translated is obj attribute not keyword
        if ('translated' not in key) and (key not in _NSXV_SUPPORTED_KEYWORDS):
          unsupported_keywords.append(key)
    if unsupported_keywords:
      logging.warning('WARNING: The keywords %s in Term %s are not supported '
                      'in Nsxv ', unsupported_keywords, self.term.name)

    name = '%s%s%s' % (_XML_TABLE.get('nameStart'), self.term.name,
                       _XML_TABLE.get('nameEnd'))

    notes = ''
    if self.term.comment:
      for comment in self.term.comment:
        notes = '%s%s' %(notes, comment)
      notes = '%s%s%s' % (_XML_TABLE.get('noteStart'), notes,
                          _XML_TABLE.get('noteEnd'))

    # protocol
    protocol = None

    if self.term.protocol:
      protocol = list(map(self.PROTO_MAP.get, self.term.protocol,
                          self.term.protocol))

      # icmp-types
      icmp_types = ['']
      if self.term.icmp_type:
        icmp_types = self.NormalizeIcmpTypes(self.term.icmp_type,
                                             self.term.protocol,
                                             self.af)

    # for mixed filter type get both IPV4address and IPv6Address
    af_list = []
    if self.filter_type == 'mixed':
      af_list = [4, 6]
    else:
      af_list = [self.af]

    source_address = None
    destination_address = None
    source_addr = []
    destination_addr = []

    source_v4_addr = []
    source_v6_addr = []
    dest_v4_addr = []
    dest_v6_addr = []

    for af in af_list:
      # source address
      if self.term.source_address:
        source_address = self.term.GetAddressOfVersion('source_address', af)
        source_address_exclude = self.term.GetAddressOfVersion(
            'source_address_exclude', af)
        if source_address_exclude:
          source_address = nacaddr.ExcludeAddrs(
              source_address,
              source_address_exclude)

        if source_address:
          if af == 4:
            source_v4_addr = source_address
          else:
            source_v6_addr = source_address
        source_addr = source_v4_addr + source_v6_addr

      # destination address
      if self.term.destination_address:
        destination_address = self.term.GetAddressOfVersion(
            'destination_address', af)
        destination_address_exclude = self.term.GetAddressOfVersion(
            'destination_address_exclude', af)
        if destination_address_exclude:
          destination_address = nacaddr.ExcludeAddrs(
              destination_address,
              destination_address_exclude)

        if destination_address:
          if af == 4:
            dest_v4_addr = destination_address
          else:
            dest_v6_addr = destination_address
        destination_addr = dest_v4_addr + dest_v6_addr

    # Check for mismatch IP for source and destination address for mixed filter
    if self.filter_type == 'mixed':
      if source_addr and destination_addr:
        if source_v4_addr and not dest_v4_addr:
          source_addr = source_v6_addr
        elif source_v6_addr and not dest_v6_addr:
          source_addr = source_v4_addr
        elif dest_v4_addr and not source_v4_addr:
          destination_addr = dest_v6_addr
        elif dest_v6_addr and not source_v6_addr:
          destination_addr = dest_v4_addr

        if not source_addr or not destination_addr:
          logging.warning('Term %s will not be rendered as it has IPv4/IPv6 '
                          'mismatch for source/destination for mixed address '
                          'family.', self.term.name)
          return ''

    # ports
    source_port = None
    destination_port = None
    if self.term.source_port:
      source_port = self.term.source_port
    if self.term.destination_port:
      destination_port = self.term.destination_port

    # logging
    log = 'false'
    if self.term.logging:
      log = 'true'

    sources = ''
    if source_addr:
      sources = '<sources excluded="false">'
      for saddr in source_addr:

        # inet4
        if isinstance(saddr, nacaddr.IPv4):
          if saddr.num_addresses > 1:
            saddr = '%s%s%s' % (_XML_TABLE.get('srcIpv4Start'),
                                saddr.with_prefixlen,
                                _XML_TABLE.get('srcIpv4End'),)
          else:
            saddr = '%s%s%s' % (_XML_TABLE.get('srcIpv4Start'),
                                saddr.network_address,
                                _XML_TABLE.get('srcIpv4End'))
          sources = '%s%s' %(sources, saddr)
        # inet6
        if isinstance(saddr, nacaddr.IPv6):
          if saddr.num_addresses > 1:
            saddr = '%s%s%s' % (_XML_TABLE.get('srcIpv6Start'),
                                saddr.with_prefixlen,
                                _XML_TABLE.get('srcIpv6End'),)
          else:
            saddr = '%s%s%s' % (
                _XML_TABLE.get('srcIpv6Start'),
                saddr.network_address, _XML_TABLE.get('srcIpv6End'))
          sources = '%s%s' %(sources, saddr)
      sources = '%s%s' %(sources, '</sources>')

    destinations = ''
    if destination_addr:
      destinations = '<destinations excluded="false">'
      for daddr in destination_addr:
        # inet4
        if isinstance(daddr, nacaddr.IPv4):
          if daddr.num_addresses > 1:
            daddr = '%s%s%s' % (_XML_TABLE.get('destIpv4Start'),
                                daddr.with_prefixlen,
                                _XML_TABLE.get('destIpv4End'),)
          else:
            daddr = '%s%s%s' % (_XML_TABLE.get('destIpv4Start'),
                                daddr.network_address,
                                _XML_TABLE.get('destIpv4End'))
          destinations = '%s%s' %(destinations, daddr)
        # inet6
        if isinstance(daddr, nacaddr.IPv6):
          if daddr.num_addresses > 1:
            daddr = '%s%s%s' % (_XML_TABLE.get('destIpv6Start'),
                                daddr.with_prefixlen,
                                _XML_TABLE.get('destIpv6End'),)
          else:
            daddr = '%s%s%s' % (_XML_TABLE.get('destIpv6Start'),
                                daddr.network_address,
                                _XML_TABLE.get('destIpv6End'))
          destinations = '%s%s' %(destinations, daddr)
      destinations = '%s%s' %(destinations, '</destinations>')

    services = []
    if protocol:
      services.append('<services>')
      for proto in protocol:
        if proto != 'any':
          services.append(self._ServiceToString(proto,
                                                source_port,
                                                destination_port,
                                                icmp_types))
      services.append('</services>')

    service = ''
    for s in services:
      service = '%s%s' % (service, s)

    # applied_to
    applied_to_list = ''
    if self.applied_to:
      applied_to_list = '<appliedToList>'
      applied_to_element = '%s%s%s' % (_XML_TABLE.get('appliedToStart'),
                                       self.applied_to,
                                       _XML_TABLE.get('appliedToEnd'))
      applied_to_list = '%s%s' %(applied_to_list, applied_to_element)
      applied_to_list = '%s%s' %(applied_to_list, '</appliedToList>')

    # action
    action = '%s%s%s' % (_XML_TABLE.get('actionStart'),
                         _ACTION_TABLE.get(str(self.term.action[0])),
                         _XML_TABLE.get('actionEnd'))

    ret_lines = []
    ret_lines.append('<rule logged="%s">%s%s%s%s%s%s%s</rule>' %
                     (log, name, action, sources, destinations, service,
                      applied_to_list, notes))

    # remove any trailing spaces and replace multiple spaces with singles
    stripped_ret_lines = [re.sub(r'\s+', ' ', x).rstrip() for x in ret_lines]
    ret_str.extend(stripped_ret_lines)
    return ''.join(ret_str)

  def _ServiceToString(self, proto, sports, dports, icmp_types):
    """Converts service to string.

    Args:
      proto: str, protocl
      sports: str list or none, the source port
      dports: str list or none, the destination port
      icmp_types: icmp-type numeric specification (if any)

    Returns:
      Service definition.

    """
    service = ''
    # for icmp and icmpv6
    if proto == 1 or proto == 58:
      # handle icmp protocol
      for icmp_type in icmp_types:
        icmp_service = '%s%s%s%s' % (_XML_TABLE.get('serviceStart'),
                                     _XML_TABLE.get('protocolStart'), proto,
                                     _XML_TABLE.get('protocolEnd'))
        # handle icmp types
        if icmp_type:
          icmp_type = '%s%s%s' %(_XML_TABLE.get('icmpTypeStart'),
                                 str(icmp_type),
                                 _XML_TABLE.get('icmpTypeEnd'))
          icmp_service = '%s%s' % (icmp_service, icmp_type)
        icmp_service = '%s%s' % (icmp_service, _XML_TABLE.get('serviceEnd'))
        service = '%s%s' % (service, icmp_service)
    else:
      # handle other protocols
      service = '%s%s%s%s' % (_XML_TABLE.get('serviceStart'),
                              _XML_TABLE.get('protocolStart'), proto,
                              _XML_TABLE.get('protocolEnd'))

      # handle source ports
      if sports:
        str_sport = []
        for sport in sports:
          if sport[0] != sport[1]:
            str_sport.append('%s-%s' % (sport[0], sport[1]))
          else:
            str_sport.append('%s' % (sport[0]))
        service = '%s%s%s%s' % (service, _XML_TABLE.get('srcPortStart'),
                                ', '.join(str_sport),
                                _XML_TABLE.get('srcPortEnd'))

      # handle destination ports
      if dports:
        str_dport = []
        for dport  in dports:
          if dport[0] != dport[1]:
            str_dport.append('%s-%s' % (dport[0], dport[1]))
          else:
            str_dport.append('%s' % (dport[0]))
        service = '%s%s%s%s' % (service, _XML_TABLE.get('destPortStart'),
                                ', '.join(str_dport),
                                _XML_TABLE.get('destPortEnd'))
      service = '%s%s' % (service, _XML_TABLE.get('serviceEnd'))

    return service


class Nsxv(aclgenerator.ACLGenerator):
  """Nsxv rendering class.

    This class takes a policy object and renders the output into a syntax
    which is understood by nsxv policy.

  Attributes:
    pol: policy.Policy object

  Raises:
  UnsupportedNsxvAccessListError: Raised when we're give a non named access
  list.

  """

  _PLATFORM = 'nsxv'
  _DEFAULT_PROTOCOL = 'ip'
  SUFFIX = '.nsx'

  _OPTIONAL_SUPPORTED_KEYWORDS = set(['expiration',
                                      'logging',
                                     ])
  _FILTER_OPTIONS_DICT = {}

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'logging'}
    supported_sub_tokens.update({'action': {'accept', 'deny', 'reject',
                                            'reject-with-tcp-rst'}})
    del supported_sub_tokens['option']
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.nsxv_policies = []
    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)
      if len(filter_options) >= 2:
        filter_name = filter_options[1]

      # get filter type, section id and applied To
      self._ParseFilterOptions(filter_options)

      filter_type = self._FILTER_OPTIONS_DICT['filter_type']
      applied_to = self._FILTER_OPTIONS_DICT['applied_to']

      term_names = set()
      new_terms = []
      for term in terms:
        # Check for duplicate terms
        if term.name in term_names:
          raise NsxvDuplicateTermError('There are multiple terms named: %s' %
                                       term.name)
        term_names.add(term.name)

        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue
        # Get the mapped action value
        # If there is no mapped action value term is not rendered
        mapped_action = _ACTION_TABLE.get(str(term.action[0]))
        if not mapped_action:
          logging.warning('WARNING: Action %s in Term %s is not valid and '
                          'will not be rendered.', term.action, term.name)
          continue

        term.name = self.FixTermLength(term.name)

        if filter_type == 'inet':
          af = 'inet'
          term = self.FixHighPorts(term, af=af)
          if not term:
            continue
          new_terms.append(Term(term, filter_type, applied_to, 4))

        if filter_type == 'inet6':
          af = 'inet6'
          term = self.FixHighPorts(term, af=af)
          if not term:
            continue
          new_terms.append(Term(term, filter_type, applied_to, 6))

        if filter_type == 'mixed':
          if 'icmpv6' not in term.protocol:
            inet_term = self.FixHighPorts(term, 'inet')
            if not inet_term:
              continue
            new_terms.append(Term(inet_term, filter_type, applied_to, 4))
          else:
            inet6_term = self.FixHighPorts(term, 'inet6')
            if not inet6_term:
              continue
            new_terms.append(Term(inet6_term, filter_type, applied_to, 6))

      self.nsxv_policies.append((header, filter_name, [filter_type],
                                 new_terms))

  def _ParseFilterOptions(self, filter_options):
    """Parses the target in header for filter type, section_id and applied_to.

    Args:
      filter_options: list of remaining target options

    Returns:
      A dictionary that contains fields necessary to create the firewall
      rule.

    Raises:
      UnsupportedNsxvAccessListError: Raised when we're give a non named access
      list.
    """
    # check for filter type
    if not 2 <= len(filter_options) <= 5:
      raise UnsupportedNsxvAccessListError(
          'Invalid Number of options specified: %d. Required options '
          'are: filter type and section name. Platform: %s' % (
              len(filter_options), self._PLATFORM))
    # mandatory section_name
    section_name = filter_options[0]
    # mandatory
    filter_type = filter_options[1]

    # a mixed filter outputs both ipv4 and ipv6 acls in the same output file
    good_filters = ['inet', 'inet6', 'mixed']

    # check if filter type is renderable
    if filter_type not in good_filters:
      raise UnsupportedNsxvAccessListError(
          'Access list type %s not supported by %s (good types: %s)' % (
              filter_type, self._PLATFORM, str(good_filters)))

    section_id = 0
    applied_to = None
    filter_opt_len = len(filter_options)

    if filter_opt_len > 2:
      for index in range(2, filter_opt_len):
        if index == 2 and filter_options[2] != 'securitygroup':
          section_id = filter_options[2]
          continue
        if filter_options[index] == 'securitygroup':
          if index + 1 <= filter_opt_len - 1:
            applied_to = filter_options[index + 1]
            break
          else:
            raise UnsupportedNsxvAccessListError(
                'Security Group Id is not provided for %s' % (self._PLATFORM))

    self._FILTER_OPTIONS_DICT['section_name'] = section_name
    self._FILTER_OPTIONS_DICT['filter_type'] = filter_type
    self._FILTER_OPTIONS_DICT['section_id'] = section_id
    self._FILTER_OPTIONS_DICT['applied_to'] = applied_to

  def __str__(self):
    """Render the output of the Nsxv policy."""

    target_header = []
    target = []

    # add the p4 tags
    target.append('<!--')
    target.extend(aclgenerator.AddRepositoryTags('\n'))
    target.append('\n')
    target.append('-->')

    for (_, _, _, terms) in self.nsxv_policies:
      section_name = six.ensure_str(self._FILTER_OPTIONS_DICT['section_name'])
      # check section id value
      section_id = self._FILTER_OPTIONS_DICT['section_id']
      if not section_id or section_id == 0:
        logging.warning('WARNING: Section-id is 0. A new Section is created '
                        'for %s. If there is any existing section, it '
                        'will remain unreferenced and should be removed '
                        'manually.', section_name)
        target.append('<section name="%s">' % (section_name.strip(' \t\n\r')))
      else:
        target.append('<section id="%s" name="%s">' %
                      (section_id, section_name.strip(' \t\n\r')))

      # now add the terms
      for term in terms:
        term_str = str(term)
        if term_str:
          target.append(term_str)

      # ensure that the header is always first
      target = target_header + target
      target.append('%s' % (_XML_TABLE.get('sectionEnd')))
      target.append('\n')

      target_as_xml = xml.dom.minidom.parseString(''.join(target))
    # TODO(robankeny) utf encoding with refactoring after migration to py3
    return target_as_xml.toprettyxml(indent='  ')


# capirca/lib/openconfig.py

# Copyright 2021 Google 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.
#

"""Openconfig yang ACL generator.

More information about the Openconfig ACL model schema:
http://ops.openconfig.net/branches/models/master/openconfig-acl.html
"""

import copy
import datetime
import ipaddress
import json
import logging
import re

from typing import Dict, Any

from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from collections import defaultdict
import six


class Error(Exception):
  """Generic error class."""


class OcFirewallError(Error):
  """Raised with problems in formatting for OpenConfig firewall."""


class ExceededAttributeCountError(Error):
  """Raised when the total attribute count of a policy is above the maximum."""


# Graceful handling of dict heierarchy for OpenConfig JSON.
def RecursiveDict():
  return defaultdict(RecursiveDict)


class Term(aclgenerator.Term):
  """Creates the term for the OpenConfig firewall."""

  ACTION_MAP = {'accept': 'ACCEPT',
                'deny': 'DROP',
                'reject': 'REJECT'}

  # OpenConfig ip-protocols always will resolve to an 8-bit int, but these
  # common names are more convenient in a policy file.
  _ALLOW_PROTO_NAME = frozenset(
      ['tcp', 'udp', 'icmp', 'esp', 'ah', 'ipip', 'sctp'])

  AF_RENAME = { 4: 'ipv4',
                6: 'ipv6',
              }

  def __init__(self, term, inet_version='inet'):
    super().__init__(term)
    self.term = term
    self.inet_version = inet_version

    # Combine (flatten) addresses with their exclusions into a resulting
    # flattened_saddr, flattened_daddr, flattened_addr.
    self.term.FlattenAll()

  def __str__(self):
    """Convert term to a string."""
    rules = self.ConvertToDict()
    json.dumps(rules, indent=2)

  def ConvertToDict(self):
    """Convert term to a dictionary.

    This is used to get a dictionary describing this term which can be
    output easily as an Openconfig JSON blob. It represents an "acl-entry"
    message from the OpenConfig ACL schema.

    Returns:
      A list of dictionaries that contains all fields necessary to create or
      update a OpenConfig acl-entry.
    """
    term_dict = RecursiveDict()

    # Rules will hold all exploded acl-entry dictionaries.
    rules = []

    # Convert the integer to the proper openconfig schema name str, ipv4/ipv6.
    term_af = self.AF_MAP.get(self.inet_version)
    family = self.AF_RENAME[term_af]

    # Action
    action = self.ACTION_MAP[self.term.action[0]]
    term_dict['actions'] = {}
    term_dict['actions']['forwarding-action'] = action

    # Ballot fatigue handling for 'any'.
    saddrs = self.term.GetAddressOfVersion('flattened_saddr', term_af)
    if not saddrs:
      saddrs = ['any']

    daddrs = self.term.GetAddressOfVersion('flattened_daddr', term_af)
    if not daddrs:
      daddrs = ['any']

    sports = self.term.source_port
    if not sports:
      sports = [(0,0)]

    dports = self.term.destination_port
    if not dports:
      dports = [(0,0)]

    protos = self.term.protocol
    if not protos:
      protos = ['none']

    ace_dict = copy.deepcopy(term_dict)
    # Source Addresses
    for saddr in saddrs:
      if saddr != 'any':
        ace_dict[family]['config']['source-address'] = str(saddr)

      # Destination Addresses
      for daddr in daddrs:
        if daddr != 'any':
          ace_dict[family]['config']['destination-address'] = str(daddr)

        # Source Port
        for start, end in sports:
          # 'any' starts and ends with zero.
          if not start == end == 0:
            if start == end:
              ace_dict[family]['transport']['config']['source-port'] = int(start)
            else:
              ace_dict[family]['transport']['config']['source-port'] = '%d..%d' % (start, end)

          # Destination Port
          for start, end in dports:
            if not start == end == 0:
              if start == end:
                ace_dict[family]['transport']['config']['destination-port'] = int(start)
              else:
                ace_dict[family]['transport']['config']['destination-port'] = '%d..%d' % (start, end)

            # Protocol
            for proto in protos:
              if isinstance(proto, str):
                if proto != 'none':
                  try:
                    proto_num = self.PROTO_MAP[proto]
                  except KeyError:
                    raise OcFirewallError(
                        'Protocol %s unknown. Use an integer.', proto)
                  ace_dict[family]['config']['protocol'] = proto_num
                rules.append(copy.deepcopy(ace_dict))
              else:
                proto_num = proto
                ace_dict[family]['config']['protocol'] = proto_num
                # This is the business end of ace explosion.
                # A dict is a reference type, so deepcopy is atually required.
                rules.append(copy.deepcopy(ace_dict))

    return rules


class OpenConfig(aclgenerator.ACLGenerator):
  """A OpenConfig firewall policy object."""

  _PLATFORM = 'openconfig'
  SUFFIX = '.oacl'
  _SUPPORTED_AF = frozenset(('inet', 'inet6', 'mixed'))

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    # Remove unsupported things
    supported_tokens -= {'platform',
                         'platform_exclude',
                         'icmp-type',
                         'verbatim'}

    # OpenConfig ACL model only supports these three forwarding actions.
    supported_sub_tokens['action'] = {'accept', 'deny', 'reject'}

    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.oc_policies = []
    total_rule_count = 0

    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)
      filter_name = header.FilterName(self._PLATFORM)

      # Options are anything after the platform name in the target message of
      # the policy header, [1:].

      # Get the address family if set.
      address_family = 'inet'
      for i in self._SUPPORTED_AF:
        if i in filter_options:
          address_family = i
          filter_options.remove(i)

      for term in terms:

        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue
        # TODO(b/196430344): Add support for options such as
        # established/rst/first-fragment
        if term.option:
          raise OcFirewallError(
              'OpenConfig firewall does not support term options.')

        # Handle mixed for each indvidual term as inet and inet6.
        # inet/inet6 are treated the same.
        term_address_families = []
        if address_family == 'mixed':
          term_address_families = ['inet', 'inet6']
        else:
          term_address_families = [address_family]
        for term_af in term_address_families:
          t = Term(term, term_af)
          for rule in t.ConvertToDict():
            total_rule_count += 1
            self.oc_policies.append(rule)

    logging.info('Total rule count of policy %s is: %d', filter_name,
                 total_rule_count)


  def __str__(self):
    out = '%s\n\n' % (json.dumps(self.oc_policies, indent=2,
                                 separators=(six.ensure_str(','),
                                             six.ensure_str(': ')),
                                 sort_keys=True))

    return out


# capirca/lib/packetfilter.py

# Copyright 2012 Google 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.
#

"""PacketFilter (PF) generator."""

import collections
import copy
import datetime
from typing import cast

from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import nacaddr


class Error(Exception):
  """Base error class."""


class DuplicateTermError(Error):
  """Raised when duplication of term names are detected."""


class DuplicateShortenedTableNameError(Error):
  """Raised when a duplicate shortened table name is found."""


class UnsupportedProtoError(Error):
  """Raised when a protocol is not supported."""


class Term(aclgenerator.Term):
  """Generate PacketFilter policy terms."""

  # Validate that term does not contain any fields we do not
  # support.  This prevents us from thinking that our output is
  # correct in cases where we've omitted fields from term.
  _PLATFORM = 'packetfilter'
  _ACTION_TABLE = {
      'accept': 'pass',
      'deny': 'block drop',
      'reject': 'block return',
      'next': 'pass',
      }
  # Moving the log keyword into an member variable allows subclasses to override
  # it to support logging options outside of the scope of the capirca policy
  # spec, e.g., on platform-specific options such as packetfilter's
  # "log (all, to pflogN)" per-direction.
  _LOG_TABLE = {
      '': 'log',
      'in': 'log',
      'out': 'log',
      }
  _QUICK_TABLE = {
      'accept': 'quick',
      'deny': 'quick',
      'reject': 'quick',
      'next': '',
      }
  _DIRECTION_TABLE = {
      '': '',
      'in': 'in',
      'out': 'out',
      }
  _TCP_FLAGS_TABLE = {
      'syn': 'S',
      'ack': 'A',
      'fin': 'F',
      'rst': 'R',
      'urg': 'U',
      'psh': 'P',
      'all': 'SAFRUP',
      'none': 'NONE',
      }
  _PROTO_TABLE = {
      'icmpv6': 'ipv6-icmp',
      }
  _UNSUPPORTED_PROTOS = ['hopopt']

  def __init__(self, term, filter_name, stateful=True, af='inet', direction=''):
    """Setup a new term.

    Args:
      term: A policy.Term object to represent in packetfilter.
      filter_name: The name of the filter chan to attach the term to.
      stateful: Whether to keep firewall state for the term.
      af: Which address family ('inet' or 'inet6') to apply the term to.
      direction: What direction the term applies to ('in', 'out' or both).

    Raises:
      aclgenerator.UnsupportedFilterError: Filter is not supported.
    """
    super().__init__(term)
    self.term = term  # term object
    self.filter = filter_name  # actual name of filter
    self.options = []
    self.default_action = 'deny'
    self.af = af
    self.stateful = stateful
    self.direction = direction

  def __str__(self):
    """Render config output from this term object."""

    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if self._PLATFORM not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if self._PLATFORM in self.term.platform_exclude:
        return ''

    ret_str = []
    self._SetDefaultAction()

    # Create a new term
    ret_str.append('\n# term %s' % self.term.name)

    comments = aclgenerator.WrapWords(self.term.comment, 80)
    # append comments to output
    if comments and comments[0]:
      for line in comments:
        ret_str.append('# %s' % str(line))

    if str(self.term.action[0]) not in self._ACTION_TABLE:
      raise aclgenerator.UnsupportedFilterError('%s %s %s %s' % (
          '\n', self.term.name, self.term.action[0],
          'action not currently supported.'))

    if self.direction and str(self.direction) not in self._DIRECTION_TABLE:
      raise aclgenerator.UnsupportedFilterError('%s %s %s %s' % (
          '\n', self.term.name, self.term.direction,
          'direction not currently supported.'))
    # protocol
    if self.term.protocol:
      protocol = self.term.protocol
    else:
      protocol = []

    # source address
    term_saddrs = self._CheckAddressAf(self.term.source_address)
    if not term_saddrs:
      logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                   direction='source',
                                                   af=self.af))
      return ''
    term_saddr = self._GenerateAddrStatement(
        term_saddrs, self.term.source_address_exclude)

    # destination address
    term_daddrs = self._CheckAddressAf(self.term.destination_address)
    if not term_daddrs:
      logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                   direction='destination',
                                                   af=self.af))
      return ''
    term_daddr = self._GenerateAddrStatement(
        term_daddrs, self.term.destination_address_exclude)

    # ports
    source_port = []
    destination_port = []
    if self.term.source_port:
      source_port = self._GeneratePortStatement(self.term.source_port)
    if self.term.destination_port:
      destination_port = self._GeneratePortStatement(self.term.destination_port)

    # icmp-type
    icmp_types = ['']
    if self.term.icmp_type:
      if protocol == ['icmp']:
        af = 'inet'
      elif protocol == ['icmpv6']:
        af = 'inet6'
      else:
        raise aclgenerator.UnsupportedFilterError('%s %s %s' % (
            '\n', self.term.name,
            'icmp protocol is not defined or not supported.'))
      icmp_types = self.NormalizeIcmpTypes(self.term.icmp_type, protocol, af)

    # options
    tcp_flags_set = []
    tcp_flags_check = []
    for next_opt in [str(x) for x in self.term.option]:
      for next_flag in self._TCP_FLAGS_TABLE:
        if next_opt.find(next_flag) == 0:
          if protocol != ['tcp']:
            raise aclgenerator.UnsupportedFilterError('%s %s %s' % (
                '\n', self.term.name,
                'tcp flags may only be specified with tcp protocol.'))
          tcp_flags_set.append(self._TCP_FLAGS_TABLE.get(next_flag))
          tcp_flags_check.append(self._TCP_FLAGS_TABLE.get(next_flag))

    # If tcp-established is set, override any of the flags above with the
    # S/SA flags.  Issue an error if flags are specified with 'established'.
    for opt in [str(x) for x in self.term.option]:
      if opt == 'established' or opt == 'tcp-established':
        if tcp_flags_set or tcp_flags_check:
          raise aclgenerator.UnsupportedFilterError('%s %s %s' % (
              '\n', self.term.name,
              'tcp flags may not be specified with tcp-established.'))
        # We need to set 'flags A/A' for established regardless of whether or
        # not we're stateful:
        # - if we stateful, the default is 'flags S/SA' which prevent writing
        # rules for reply packets.
        # - if we're stateless, this is the only way to do it.
        if not protocol or 'tcp' in protocol:
          tcp_flags_set.append(self._TCP_FLAGS_TABLE.get('ack'))
          tcp_flags_check.append(self._TCP_FLAGS_TABLE.get('ack'))

    # The default behavior of pf is 'keep state flags S/SA'.  If we're not
    # stateless, and if flags have not been specified explicitly via options,
    # append that here.  Note that pf allows appending flags for udp and icmp;
    # they are just ignored, as long as TCP is in the proto.  This lets you
    # doing things like 'proto { tcp udp icmp } flags S/SA' and have the flags
    # only applied to the tcp bits that match.  However, the policy description
    # language prohibits setting flags on non-TCP, since it doesn't make sense
    # on all platforms.
    if ((not protocol or protocol == ['tcp']) and self.stateful
        and not tcp_flags_set and not tcp_flags_check):
      tcp_flags_set.append(self._TCP_FLAGS_TABLE.get('syn'))
      tcp_flags_check.append(self._TCP_FLAGS_TABLE.get('syn'))
      tcp_flags_check.append(self._TCP_FLAGS_TABLE.get('ack'))

    ret_str.extend(self._FormatPart(
        self.term.action[0],
        self.direction,
        self.term.logging,
        self.term.interface,
        self.af,
        protocol,
        term_saddr,
        source_port,
        term_daddr,
        destination_port,
        tcp_flags_set,
        tcp_flags_check,
        icmp_types,
        self.options,
        self.stateful,))

    return '\n'.join(str(v) for v in ret_str if v)

  def _CheckAddressAf(self, addrs):
    """Verify that the requested address-family matches the address's family."""
    if not addrs:
      return ['any']
    if self.af == 'mixed':
      return addrs
    af_addrs = []
    af = self.NormalizeAddressFamily(self.af)
    for addr in addrs:
      if addr.version == af:
        af_addrs.append(addr)
    return af_addrs

  def _FormatPart(self, action, direction, log, interface, af, proto, src_addr,
                  src_port, dst_addr, dst_port, tcp_flags_set, tcp_flags_check,
                  icmp_types, options, stateful):
    """Format the string which will become a single PF entry."""
    line = ['%s' % self._ACTION_TABLE.get(action)]

    if direction:
      line.append(direction)

    quick = self._QUICK_TABLE.get(action)
    if quick:
      line.append('%s' % quick)

    if log:
      logaction = self._LOG_TABLE.get(direction)
      if logaction:
        line.append(logaction)
      else:
        line.append('log')

    if interface:
        line.append('on %s' % interface)

    if af != 'mixed':
      line.append(af)

    if proto:
      line.append(self._GenerateProtoStatement(proto))

    line.append('from %s' % src_addr)
    if src_port:
      line.append('port %s' % src_port)

    line.append('to %s' % dst_addr)
    if dst_port:
      line.append('port %s' % dst_port)

    if tcp_flags_set and tcp_flags_check:
      line.append('flags')
      line.append('%s/%s' % (''.join(tcp_flags_set), ''.join(tcp_flags_check)))

    if 'icmp' in proto and icmp_types:
      type_strs = [str(icmp_type) for icmp_type in icmp_types]
      type_strs = ', '.join(type_strs)
      if type_strs:
        line.append('icmp-type { %s }' % type_strs)

    if 'icmpv6' in proto and icmp_types:
      type_strs = [str(icmp_type) for icmp_type in icmp_types]
      type_strs = ', '.join(type_strs)
      if type_strs:
        line.append('icmp6-type { %s }' % type_strs)

    if options:
      line.extend(options)

    if not stateful:
      line.append('no state')
    elif action in ['accept', 'next']:
      line.append('keep state')

    return [' '.join(line)]

  def _GenerateProtoStatement(self, protocols):
    proto = ''
    if protocols:
      protocols = copy.deepcopy(protocols)
      for i, proto in enumerate(protocols):
        if proto in self._UNSUPPORTED_PROTOS:
          raise UnsupportedProtoError
        try:
          protocols[i] = self._PROTO_TABLE[proto]
        except KeyError:
          pass
      proto = 'proto { %s }' % ' '.join(protocols)
    return proto

  def _GenerateAddrStatement(self, addrs, exclude_addrs):
    addresses = set()
    if addrs != ['any']:
      parent_token_set = set()
      for addr in addrs:
        addr = cast(nacaddr.IPType, addr)
        parent_token_set.add(addr.parent_token)
      for token in parent_token_set:
        addresses.add('<%s>' % token[:31])
    else:
      addresses.add('any')
    if exclude_addrs != ['any']:
      parent_token_set = set()
      for addr in exclude_addrs:
        addr = cast(nacaddr.IPType, addr)
        parent_token_set.add(addr.parent_token)
      for token in parent_token_set:
        addresses.add('!<%s>' % token[:31])
    return '{ %s }' % ', '.join(sorted(addresses))

  def _GeneratePortStatement(self, ports):
    port_list = []
    for port_tuple in ports:
      if port_tuple[0] == port_tuple[1]:
        port_list.append(str(port_tuple[0]))
      else:
        port_list.append('%s:%s' % (port_tuple[0], port_tuple[1]))
    return '{ %s }' % (
        ' '.join(list(collections.OrderedDict.fromkeys(port_list))))

  def _SetDefaultAction(self):
    """If term does not specify action, use filter default action."""
    if not self.term.action:
      self.term.action[0].value = self.default_action


class PacketFilter(aclgenerator.ACLGenerator):
  """Generates filters and terms from provided policy object."""

  _DEF_MAX_LENGTH = 31
  _PLATFORM = 'packetfilter'
  _DEFAULT_PROTOCOL = 'all'
  SUFFIX = '.pf'
  _TERM = Term

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'logging', 'destination_interface', 'source_interface'}
    supported_sub_tokens.update({
        'action': {'accept', 'deny', 'reject', 'next'},
        'option': {
            'established',
            'tcp-established',
            'syn',
            'ack',
            'fin',
            'rst',
            'urg',
            'psh',
            'all'},
    })

    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.pf_policies = []
    self.address_book = {}
    self.def_short_to_long = {}
    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    good_afs = ['inet', 'inet6', 'mixed']
    good_options = ['in', 'out', 'nostate']
    all_protocols_stateful = True

    for header, terms in pol.filters:
      filter_type = None
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)[1:]
      filter_name = header.FilterName(self._PLATFORM)
      direction = ''

      # ensure all options after the filter name are expected
      for opt in filter_options:
        if opt not in good_afs + good_options:
          raise aclgenerator.UnsupportedTargetOptionError('%s %s %s %s' % (
              '\nUnsupported option found in', self._PLATFORM,
              'target definition:', opt))

      # pf will automatically add 'keep state flags S/SA' to all TCP connections
      # by default.
      if 'nostate' in filter_options:
        all_protocols_stateful = False

      if 'in' in filter_options:
        direction = 'in'
      elif 'out' in filter_options:
        direction = 'out'

      # Check for matching af
      for address_family in good_afs:
        if address_family in filter_options:
          # should not specify more than one AF in options
          if filter_type is not None:
            raise aclgenerator.UnsupportedFilterError('%s %s %s %s' % (
                '\nMay only specify one of', good_afs, 'in filter options:',
                filter_options))
          filter_type = address_family
      if filter_type is None:
        filter_type = 'inet'

      # add the terms
      new_terms = []
      term_names = set()

      for term in terms:
        term.name = self.FixTermLength(term.name)
        if term.name in term_names:
          raise DuplicateTermError(
              'You have a duplicate term: %s' % term.name)
        term_names.add(term.name)

        for source_addr in term.source_address:
          src_token = source_addr.parent_token[:self._DEF_MAX_LENGTH]

          if (src_token in self.def_short_to_long and
              self.def_short_to_long[src_token] != source_addr.parent_token):
            raise DuplicateShortenedTableNameError(
                'There is a shortened name conflict between names %s and %s '
                '(different named objects would conflict when shortened to %s)'
                % (self.def_short_to_long[src_token],
                   source_addr.parent_token,
                   src_token))
          else:
            self.def_short_to_long[src_token] = source_addr.parent_token

          if src_token not in self.address_book:
            self.address_book[src_token] = set([source_addr])
          else:
            self.address_book[src_token].add(source_addr)

        for dest_addr in term.destination_address:
          dst_token = dest_addr.parent_token[:self._DEF_MAX_LENGTH]

          if (dst_token in self.def_short_to_long and
              self.def_short_to_long[dst_token] != dest_addr.parent_token):
            raise DuplicateShortenedTableNameError(
                'There is a shortened name conflict between names %s and %s '
                '(different named objects would conflict when shortened to %s)'
                %(self.def_short_to_long[dst_token],
                  dest_addr.parent_token,
                  dst_token))
          else:
            self.def_short_to_long[dst_token] = dest_addr.parent_token

          if dst_token not in self.address_book:
            self.address_book[dst_token] = set([dest_addr])
          else:
            self.address_book[dst_token].add(dest_addr)

        if not term:
          continue

        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue

        if term.destination_interface and term.source_interface:
            raise Error("packetfilter only supports destination interface XOR source interface per term")

        term_direction = direction
        if term.destination_interface:
          term_direction = 'out'
          term.interface = term.destination_interface
        elif term.source_interface:
          term_direction = 'in'
          term.interface = term.source_interface
        else:
          term.interface = None

        new_terms.append(self._TERM(term, filter_name, all_protocols_stateful,
                                    filter_type, term_direction))

      self.pf_policies.append((header, filter_name, filter_type, new_terms))

  def __str__(self):
    """Render the output of the PF policy into config."""
    target = []
    pretty_platform = '%s%s' % (self._PLATFORM[0].upper(), self._PLATFORM[1:])
    # Create address table.
    for name in sorted(self.address_book):
      v4 = sorted([x for x in self.address_book[name] if x.version == 4])
      v6 = sorted([x for x in self.address_book[name] if x.version == 6])
      entries = ',\\\n'.join(str(x) for x in v4 + v6)
      target.append('table <%s> {%s}' % (name, entries))
    # pylint: disable=unused-variable
    for (header, filter_name, filter_type, terms) in self.pf_policies:
      # Add comments for this filter
      target.append('# %s %s Policy' % (pretty_platform,
                                        header.FilterName(self._PLATFORM)))

      # reformat long text comments, if needed
      comments = aclgenerator.WrapWords(header.comment, 70)
      if comments and comments[0]:
        for line in comments:
          target.append('# %s' % line)
        target.append('#')
      # add the p4 tags
      target.extend(aclgenerator.AddRepositoryTags('# '))
      target.append('# ' + filter_type)

      # add the terms
      for term in terms:
        term_str = str(term)
        if term_str:
          target.append(term_str)
      target.append('')

    return '\n'.join(target)


# capirca/lib/paloaltofw.py

# Copyright 2017 Google 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.
"""Palo Alto Firewall generator."""

import collections
import copy
import datetime
import logging
import re
from xml.dom import minidom
import xml.etree.ElementTree as etree
from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import policy


class Error(Exception):
  """generic error class."""


class UnsupportedFilterError(Error):
  pass


class UnsupportedHeaderError(Error):
  pass


class PaloAltoFWDuplicateTermError(Error):
  pass


class PaloAltoFWUnsupportedProtocolError(Error):
  pass


class PaloAltoFWVerbatimError(Error):
  pass


class PaloAltoFWOptionError(Error):
  pass


class PaloAltoFWDuplicateServiceError(Error):
  pass


class PaloAltoFWNameTooLongError(Error):
  pass


class PaloAltoFWBadIcmpTypeError(Error):
  pass


class ServiceMap:
  """Manages service names across a single policy instance."""

  def __init__(self):
    self.entries = {}

  def get_service_name(self, term_name, src_ports, ports, protocol, prefix=None):
    """Returns service name based on the provided ports and protocol."""
    if (src_ports, ports, protocol) in self.entries:
      return self.entries[(src_ports, ports, protocol)]["name"]

    if prefix is None:
      prefix = "service-"
    service_name = "%s%s-%s" % (prefix, term_name, protocol)

    if len(service_name) > 63:
      raise PaloAltoFWNameTooLongError(
          "Service name must be 63 characters max: %s" % service_name)

    for _, service in self.entries.items():
      if service["name"] == service_name:
        raise PaloAltoFWDuplicateServiceError(
            "You have a duplicate service. A service named %s already exists." %
            service_name)

    self.entries[(src_ports, ports, protocol)] = {"name": service_name}
    return service_name


class Rule:
  """Extend the Term() class for PaloAlto Firewall Rules."""

  def __init__(self, from_zone, to_zone, term, service_map):
    # Palo Alto Firewall rule keys
    MAX_ZONE_LENGTH = 31

    if not from_zone or not to_zone:
      raise PaloAltoFWOptionError("Source or destination zone is empty.")
    if len(from_zone) > MAX_ZONE_LENGTH:
      x = "Source zone must be %d characters max: %s" % (MAX_ZONE_LENGTH,
                                                         from_zone)
      raise PaloAltoFWNameTooLongError(x)
    if len(to_zone) > MAX_ZONE_LENGTH:
      x = "Destination zone must be %d characters max: %s" % (MAX_ZONE_LENGTH,
                                                              to_zone)
      raise PaloAltoFWNameTooLongError(x)

    self.options = []

    while term is not None:
      x, term = self.TermToOptions(from_zone, to_zone,
                                   term, service_map)
      self.options.append(x)

  @staticmethod
  def TermToOptions(from_zone, to_zone, term, service_map):
    """Convert term to Palo Alto security rule options."""
    options = {}
    options["from_zone"] = [from_zone]
    options["to_zone"] = [to_zone]
    options["description"] = []
    options["source"] = []
    options["destination"] = []
    options["application"] = []
    options["service"] = []
    options["logging"] = []

    ACTIONS = {
      "accept": "allow",
      "deny": "deny",
      "reject": "reset-client",
      "reject-with-tcp-rst": "reset-client",
    }

    new_term = None

    def pan_ports(ports):
      x = []
      for tup in ports:
        if len(tup) > 1 and tup[0] != tup[1]:
          x.append(str(tup[0]) + "-" + str(tup[1]))
        else:
          x.append(str(tup[0]))

      return tuple(x)

    # COMMENT
    if term.comment:
      options["description"] = term.comment

    # LOGGING
    if term.logging:
      for item in term.logging:
        if item.value in ["disable"]:
          options["logging"] = ["disable"]
          break
        elif item.value in ["log-both"]:
          options["logging"].append("log-start")
          options["logging"].append("log-end")
        elif item.value in ["True", "true", "syslog", "local"]:
          options["logging"].append("log-end")

    # SOURCE-ADDRESS
    if term.source_address:
      saddr_check = set()
      for saddr in term.source_address:
        saddr_check.add(saddr.parent_token)
      saddr_check = sorted(saddr_check)
      for addr in saddr_check:
        options["source"].append(str(addr))
    # missing source handled during XML document generation

    # DESTINATION-ADDRESS
    if term.destination_address:
      daddr_check = set()
      for daddr in term.destination_address:
        daddr_check.add(daddr.parent_token)
      daddr_check = sorted(daddr_check)
      for addr in daddr_check:
        options["destination"].append(str(addr))
    # missing destination handled during XML document generation

    # ACTION
    if term.action:
      options["action"] = ACTIONS[term.action[0]]

    if term.option:
      options["option"] = term.option

    if term.pan_application:
      for pan_app in term.pan_application:
        options["application"].append(pan_app)

    if term.source_port or term.destination_port:
      src_ports = pan_ports(term.source_port)
      if term.destination_port:
        ports = pan_ports(term.destination_port)
      else:
        ports = pan_ports([("0", "65535")])

      # check to see if this service already exists
      for p in term.protocol:
        service_name = service_map.get_service_name(term.name,
                                                    src_ports, ports, p)
        if service_name not in options["service"]:
          options["service"].append(service_name)

    elif "tcp" in term.protocol or "udp" in term.protocol:
      services = {"tcp", "udp"} & set(term.protocol)
      others = set(term.protocol) - services
      if others:
        logging.info(
          "INFO: Term %s in policy %s>%s contains port-less %s "
          "with non-port protocol(s). Moving %s to a new term.",
          term.name, from_zone, to_zone,
          ', '.join(list(services)), ', '.join(list(others)))
        new_term = copy.deepcopy(term)
        new_term.protocol = list(others)
        term.protocol = list(services)
        options["application"] = []
      for p in term.protocol:
        ports = pan_ports([("0", "65535")])
        # use prefix "" to avoid service name clash with term named "any"
        service_name = service_map.get_service_name("any", (), ports, p, "")
        if service_name not in options["service"]:
          options["service"].append(service_name)

    if term.protocol:
      # Add certain protocol names as application in the application list
      # if missing.
      for proto_name in term.protocol:
        if (proto_name in ["igmp", "sctp", "gre"] and
            proto_name not in options["application"]):
          options["application"].append(proto_name)
        elif proto_name in ("ah", "esp"):
          ipsec_app_proto = "ipsec-%s" % proto_name
          if ipsec_app_proto not in options["application"]:
            options["application"].append(ipsec_app_proto)

    return options, new_term


class PaloAltoFW(aclgenerator.ACLGenerator):
  """PaloAltoFW rendering class."""

  _PLATFORM = "paloalto"
  SUFFIX = ".xml"
  _SUPPORTED_AF = set(("inet", "inet6", "mixed"))
  _AF_MAP = {"inet": (4,), "inet6": (6,), "mixed": (4, 6)}
  _TERM_MAX_LENGTH = 31
  _SUPPORTED_PROTO_NAMES = [
      "tcp",
      "udp",
      "icmp",
      "icmpv6",
      "sctp",
      "igmp",
      "gre",
      "ah",
      "esp",
  ]
  _MAX_RULE_DESCRIPTION_LENGTH = 1024
  _MAX_TAG_COMMENTS_LENGTH = 1023
  _TAG_NAME_FORMAT = "{from_zone}_{to_zone}_policy-comment-{num}"
  _MAX_RULE_SRC_DST_MEMBERS = 65535

  _ABBREVIATION_TABLE = [
      # Service abbreviations first.
      ("experiment", "EXP"),
      ("wifi-radius", "W-R"),
      ("customer", "CUST"),
      ("server", "SRV"),
      # Next, common routing terms
      ("global", "GBL"),
      ("google", "GOOG"),
      ("service", "SVC"),
      ("router", "RTR"),
      ("internal", "INT"),
      ("external", "EXT"),
      ("transit", "TRNS"),
      ("management", "MGMT"),
      # State info
      ("established", "EST"),
      ("unreachable", "UNR"),
      ("fragment", "FRAG"),
      ("accept", "ACC"),
      ("discard", "DISC"),
      ("reject", "REJ"),
      ("replies", "RPLS"),
      ("reply", "RPL"),
      ("request", "REQ"),
      # ICMP types specific
      ("inverse", "INV"),
      ("neighbor", "NBR"),
      ("discovery", "DSCVR"),
      ("advertisement", "ADV"),
      ("solicitation", "SOL"),
      ("multicast", "MCAST"),
      ("certification", "CERT"),
      ("listener", "LSNR"),
      ("address", "ADDR"),
  ]

  INDENT = "  "

  def __init__(self, pol, exp_info):
    self.pafw_policies = []
    self.addressbook = collections.OrderedDict()
    self.applications = []
    self.application_refs = {}
    self.application_groups = []
    self.pan_applications = []
    self.ports = []
    self.from_zone = ""
    self.to_zone = ""
    self.policy_name = ""
    self.config = None
    self.service_map = ServiceMap()
    super().__init__(pol, exp_info)

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens = {
        "action",
        "comment",
        "destination_address",
        "destination_address_exclude",
        "destination_port",
        "expiration",
        "icmp_type",
        "logging",
        "name",
        "option",
        "owner",
        "platform",
        "platform_exclude",
        "protocol",
        "source_address",
        "source_address_exclude",
        "source_port",
        "stateless_reply",
        "timeout",
        "pan_application",
        "translated",
    }

    supported_sub_tokens.update({
        "action": {"accept", "deny", "reject", "reject-with-tcp-rst"},
        "option": {"established", "tcp-established"},
    })
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    """Transform a policy object into a PaloAltoFW object.

    Args:
      pol: policy.Policy object
      exp_info: print a info message when a term is set to expire in that many
        weeks

    Raises:
      UnsupportedFilterError: An unsupported filter was specified
      UnsupportedHeaderError: A header option exists that is not
      understood/usable
      PaloAltoFWDuplicateTermError: Two terms were found with same name in
      same filter
      PaloAltoFWBadIcmpTypeError: The referenced ICMP type is not supported
      by the policy term.
      PaloAltoFWUnsupportedProtocolError: The term contains unsupporter protocol
      name.
    """
    current_date = datetime.date.today()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)
    first_addr_obj = None

    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue

      # The filter_options is a list of options from header, e.g.
      # ['from-zone', 'internal', 'to-zone', 'external']
      filter_options = header.FilterOptions(self._PLATFORM)

      if (len(filter_options) < 4 or filter_options[0] != "from-zone" or
          filter_options[2] != "to-zone"):
        raise UnsupportedFilterError(
            "Palo Alto Firewall filter arguments must specify from-zone and "
            "to-zone.")

      self.from_zone = filter_options[1]
      self.to_zone = filter_options[3]

      # The filter_type values are either inet, inet6, or mixed. Later, the
      # code analyzes source and destination IP addresses and determines whether
      # it is an appropriate type for the filter_type value.
      if len(filter_options) > 4:
        filter_type = filter_options[4]
      else:
        filter_type = "inet"

      if filter_type not in self._SUPPORTED_AF:
        raise UnsupportedHeaderError(
            'Palo Alto Firewall Generator invalid address family option: "%s"'
            '; expect {%s}' % (filter_type, '|'.join(self._SUPPORTED_AF)))

      valid_addr_obj = ["addr-obj", "no-addr-obj"]
      if len(filter_options) > 5 and filter_options[5] not in valid_addr_obj:
        raise UnsupportedHeaderError(
            'Palo Alto Firewall Generator invalid address objects option: "%s"'
            '; expect {%s}' % (filter_options[5], '|'.join(valid_addr_obj)))
      no_addr_obj = True if (len(filter_options) > 5 and
                             filter_options[5] == "no-addr-obj") else False
      if first_addr_obj is None:
        first_addr_obj = no_addr_obj
      if first_addr_obj != no_addr_obj:
        raise UnsupportedHeaderError(
            "Cannot mix addr-obj and no-addr-obj header option in "
            "a single policy file")

      term_dup_check = set()
      new_terms = []

      for term in terms:
        if term.stateless_reply:
          logging.warning(
              "WARNING: Term %s in policy %s>%s is a stateless reply "
              "term and will not be rendered.", term.name, self.from_zone,
              self.to_zone)
          continue
        if "established" in term.option:
          logging.warning(
              "WARNING: Term %s in policy %s>%s is a established "
              "term and will not be rendered.", term.name, self.from_zone,
              self.to_zone)
          continue
        if "tcp-established" in term.option:
          logging.warning(
              "WARNING: Term %s in policy %s>%s is a tcp-established "
              "term and will not be rendered.", term.name, self.from_zone,
              self.to_zone)
          continue

        # Verify platform specific terms. Skip whole term if platform does not
        # match.
        if term.platform:
          if self._PLATFORM not in term.platform:
            continue
        if term.platform_exclude:
          if self._PLATFORM in term.platform_exclude:
            continue

        term.name = self.FixTermLength(term.name)
        if term.name in term_dup_check:
          raise PaloAltoFWDuplicateTermError("You have a duplicate term: %s" %
                                             term.name)
        term_dup_check.add(term.name)

        services = {"tcp", "udp"} & set(term.protocol)
        others = set(term.protocol) - services
        if others and term.pan_application:
          raise UnsupportedFilterError(
            "Term %s contains non tcp, udp protocols with pan-application: %s: %s"
            "\npan-application can only be used with protocols tcp, udp" %
            (term.name, ', '.join(term.pan_application), ', '.join(term.protocol)))

        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info(
                "INFO: Term %s in policy %s>%s expires "
                "in less than two weeks.", term.name, self.from_zone,
                self.to_zone)
          if term.expiration <= current_date:
            logging.warning(
                "WARNING: Term %s in policy %s>%s is expired and "
                "will not be rendered.", term.name, self.from_zone,
                self.to_zone)
            continue

        for i in term.source_address_exclude:
          term.source_address = nacaddr.RemoveAddressFromList(
              term.source_address, i)
        for i in term.destination_address_exclude:
          term.destination_address = nacaddr.RemoveAddressFromList(
              term.destination_address, i)

        # Count the number of occurencies of a particular version of the
        # address family, i.e. v4/v6 in source and destination IP addresses.
        afc = {
            4: {
                "src": 0,
                "dst": 0
            },
            6: {
                "src": 0,
                "dst": 0
            },
        }
        # Determine the address families in the source and destination
        # addresses references in the term. Next, determine IPv4 and IPv6
        # traffic flow patterns.
        exclude_address_family = []
        flows = []
        src_any = False
        dst_any = False
        if not term.source_address:
          src_any = True
        if not term.destination_address:
          dst_any = True
        for addr in term.source_address:
          afc[addr.version]["src"] += 1
        for addr in term.destination_address:
          afc[addr.version]["dst"] += 1
        for v in [4, 6]:
          if src_any and dst_any:
            flows.append("ip%d-ip%d" % (v, v))
            continue
          if (afc[v]["src"] == 0 and not src_any) and (afc[v]["dst"] == 0 and
                                                       not dst_any):
            continue
          if (afc[v]["src"] > 0 or src_any) and (afc[v]["dst"] > 0 or dst_any):
            flows.append("ip%d-ip%d" % (v, v))
            continue
          if (afc[v]["src"] > 0 or src_any) and afc[v]["dst"] == 0:
            flows.append("ip%d-src-only" % v)
            flows.append("ip%d-only" % v)
            continue
          if afc[v]["src"] == 0 and (afc[v]["dst"] > 0 or dst_any):
            flows.append("ip%d-dst-only" % v)
            flows.append("ip%d-only" % v)

        if filter_type == "inet":
          if "icmpv6" in term.protocol:
            logging.warning(
                "WARNING: Term %s in policy %s>%s references ICMPv6 protocol, "
                "term will not be rendered.", term.name, self.from_zone,
                self.to_zone)
            continue
          if "ip4-ip4" not in flows:
            logging.warning(
                "WARNING: Term %s in policy %s>%s has one or more invalid "
                "src-dest combinations %s, term will not be rendered.",
                term.name, self.from_zone, self.to_zone, flows)
            continue
          # exclude IPv6 addresses
          exclude_address_family.append(6)
        elif filter_type == "inet6":
          if "icmp" in term.protocol:
            logging.warning(
                "WARNING: Term %s in policy %s>%s references ICMP protocol, "
                "term and will not be rendered.", term.name, self.from_zone,
                self.to_zone)
            continue
          if "ip6-ip6" not in flows:
            logging.warning(
                "WARNING: Term %s in policy %s>%s has one or more invalid "
                "src-dest combinations %s, term will not be rendered.",
                term.name, self.from_zone, self.to_zone, flows)
            continue
          exclude_address_family.append(4)
        elif filter_type == "mixed":
          if "ip4-ip4" in flows and "ip6-ip6" not in flows:
            exclude_address_family.append(6)
            pass
          elif "ip6-ip6" in flows and "ip4-ip4" not in flows:
            exclude_address_family.append(4)
            pass
          elif "ip4-ip4" in flows and "ip6-ip6" in flows:
            pass
          elif "ip4-only" in flows and "ip6-only" in flows:
            logging.warning(
                "WARNING: Term %s in policy %s>%s has source and destinations "
                "of different address families %s, term will not be "
                "rendered.", term.name, self.from_zone, self.to_zone,
                filter(lambda p: re.search(p, "(src|dst)-only"), flows))
            continue
          else:
            logging.warning(
                "WARNING: Term %s in policy %s>%s has invalid src-dest "
                "combinations %s, the term will be rendered without them.",
                term.name, self.from_zone, self.to_zone,
                filter(lambda p: re.search(p, "(src|dst)-only"), flows))
            if "ip4-ip4" in flows:
              exclude_address_family.append(6)
            else:
              exclude_address_family.append(4)

        # Substitute large IPv6 ranges (/1, /2) with equivalent subnets.
        # Do this separately from address book building, or during policy
        # translation to account for both address-objects and no-address-objects
        if term.source_address:
          saddr_split = []
          for saddr in term.source_address:
            if saddr.version == 6 and 0 < saddr.prefixlen < 3:
              for saddr2 in saddr.subnets(new_prefix=3):
                saddr2.parent_token = saddr.parent_token
                saddr_split.append(saddr2)
            else:
              saddr_split.append(saddr)
          term.source_address = saddr_split
        if term.destination_address:
          daddr_split = []
          for daddr in term.destination_address:
            if daddr.version == 6 and 0 < daddr.prefixlen < 3:
              for daddr2 in daddr.subnets(new_prefix=3):
                daddr2.parent_token = daddr.parent_token
                daddr_split.append(daddr2)
            else:
              daddr_split.append(daddr)
          term.destination_address = daddr_split

        # Build address book for the addresses referenced in the term.
        for addr in term.source_address:
          if addr.version in exclude_address_family:
            continue
          self._BuildAddressBook(self.from_zone, addr)
        for addr in term.destination_address:
          if addr.version in exclude_address_family:
            continue
          self._BuildAddressBook(self.to_zone, addr)

        # Handle ICMP/ICMPv6 terms.
        if term.icmp_type and ("icmp" not in term.protocol and
                               "icmpv6" not in term.protocol):
          raise UnsupportedFilterError(
              "Palo Alto Firewall filter must have ICMP or ICMPv6 protocol " +
              "specified when using icmp_type keyword")

        for icmp_version in ["icmp", "icmpv6"]:
          if ("icmp" not in term.protocol and "icmpv6" not in term.protocol):
            # the protocol is not ICMP or ICMPv6
            break
          if icmp_version not in term.protocol:
            # skip if this icmp_version isn't in the term protocol.
            continue
          if icmp_version == "icmp" and "ip4-ip4" not in flows:
            # skip if there is no ip4 to ipv4 communication
            continue
          if icmp_version == "icmpv6" and "ip6-ip6" not in flows:
            # skip if there is no ip4 to ipv4 communication
            continue
          if icmp_version == "icmp":
            if filter_type == "inet6":
              continue
            if not term.icmp_type:
              term.pan_application.append("icmp")
              continue
            icmp_type_keyword = "ident-by-icmp-type"
            # The risk level 4 is the default PANOS' risk level for ICMP.
            risk_level = 4
          else:
            if filter_type == "inet":
              continue
            if not term.icmp_type:
              term.pan_application.append("ipv6-icmp")
              continue
            icmp_type_keyword = "ident-by-icmp6-type"
            # The risk level 2 is the default PANOS' risk level for ICMPv6.
            risk_level = 2
          # The term contains ICMP types
          for term_icmp_type_name in term.icmp_type:
            if icmp_version == "icmp":
              icmp_app_name = "icmp-%s" % term_icmp_type_name
              # This is to abbreviate the Application name where possible.
              # The limit is defined by _TERM_MAX_LENGTH = 31.
              if len(icmp_app_name) > self._TERM_MAX_LENGTH:
                icmp_app_name = self.FixTermLength(icmp_app_name, True, True)
              if term_icmp_type_name not in policy.Term.ICMP_TYPE[4]:
                raise PaloAltoFWBadIcmpTypeError(
                    "term with bad icmp type: %s, icmp_type: %s" %
                    (term.name, term_icmp_type_name))
              term_icmp_type = policy.Term.ICMP_TYPE[4][term_icmp_type_name]
            else:
              icmp_app_name = "icmp6-%s" % term_icmp_type_name
              # This is to abbreviate the Application name where possible.
              # The limit is defined by _TERM_MAX_LENGTH = 31.
              if len(icmp_app_name) > self._TERM_MAX_LENGTH:
                icmp_app_name = self.FixTermLength(icmp_app_name, True, True)
              if term_icmp_type_name not in policy.Term.ICMP_TYPE[6]:
                raise PaloAltoFWBadIcmpTypeError(
                    "term with bad icmp type: %s, icmp_type: %s" %
                    (term.name, term_icmp_type_name))
              term_icmp_type = policy.Term.ICMP_TYPE[6][term_icmp_type_name]
            if icmp_app_name not in self.application_refs:
              # the custom icmp application doesn't already exist
              app_entry = {
                  "category": "networking",
                  "subcategory": "ip-protocol",
                  "technology": "network-protocol",
                  "description": icmp_app_name,
                  "default": {
                      icmp_type_keyword: "%d" % term_icmp_type,
                  },
                  "risk": "%d" % risk_level,
              }
              self.application_refs[icmp_app_name] = app_entry
              self.applications.append(icmp_app_name)

            # always add the ICMP application to the term, it either already
            # existed due to a previous policy, or it was created in the
            # previous loop.
            if icmp_app_name not in term.pan_application:
              term.pan_application.append(icmp_app_name)

        # Filter out unsupported protocols
        for proto_name in term.protocol:
          if proto_name in self._SUPPORTED_PROTO_NAMES:
            continue
          raise PaloAltoFWUnsupportedProtocolError(
              "protocol %s is not supported" % proto_name)

        if term.icmp_type:
          if set(term.protocol) == {'icmp', 'icmpv6'}:
            raise UnsupportedFilterError('%s %s' % (
                'icmp-type specified for both icmp and icmpv6 protocols'
                ' in a single term:', term.name))
          if term.protocol != ['icmp'] and term.protocol != ['icmpv6']:
            raise UnsupportedFilterError('%s %s' % (
                'icmp-type specified for non-icmp protocols in term:',
                term.name))

        new_terms.append(term)

      # Create a ruleset. It contains the rules for the terms defined under
      # a single header on a particular platform.
      ruleset = {}

      for term in new_terms:
        current_rule = Rule(self.from_zone, self.to_zone, term,
                            self.service_map)
        if len(current_rule.options) > 1:
          for i, v in enumerate(current_rule.options):
            name = "%s-%d" % (term.name, i+1)
            name = self.FixTermLength(name)
            ruleset[name] = v
        else:
          ruleset[term.name] = current_rule.options[0]

      self.pafw_policies.append((header, ruleset, filter_options))

  def _BuildAddressBook(self, zone, address):
    """Create the address book configuration entries.

    Args:
      zone: the zone these objects will reside in
      address: a naming library address object
    """
    if zone not in self.addressbook:
      self.addressbook[zone] = collections.OrderedDict()
    if address.parent_token not in self.addressbook[zone]:
      self.addressbook[zone][address.parent_token] = []
    name = address.parent_token
    for ip in self.addressbook[zone][name]:
      if str(address) == str(ip[0]):
        return
    counter = len(self.addressbook[zone][address.parent_token])
    name = "%s_%s" % (name, str(counter))
    self.addressbook[zone][address.parent_token].append((address, name))

  def _SortAddressBookNumCheck(self, item):
    """Used to give a natural order to the list of acl entries.

    Args:
      item: string of the address book entry name

    Returns:
      returns the characters and number
    """
    item_list = item.split("_")
    num = item_list.pop(-1)
    if isinstance(item_list[-1], int):
      set_number = item_list.pop(-1)
      num = int(set_number) * 1000 + int(num)
    alpha = "_".join(item_list)
    if num:
      return (alpha, int(num))
    return (alpha, 0)

  def _BuildPort(self, ports):
    """Transform specified ports into list and ranges.

    Args:
      ports: a policy terms list of ports

    Returns:
      port_list: list of ports and port ranges
    """
    port_list = []
    for i in ports:
      if i[0] == i[1]:
        port_list.append(str(i[0]))
      else:
        port_list.append("%s-%s" % (str(i[0]), str(i[1])))
    return port_list

  def __str__(self):
    """Render the output of the PaloAltoFirewall policy into config."""

    # IPv4 addresses are normalized into the policy as IPv6 addresses
    # using ::<ipv4-address>.  The 0.0.0.0-255.255.255.255 range is
    # equivalent to ::0/96 which will only match IPv4 addresses; when
    # negated it will match only IPv6 addresses.
    # Used for address families inet and inet6 when source and
    # destination address are not specified (any any).
    ANY_IPV4_RANGE = "0.0.0.0-255.255.255.255"
    add_any_ipv4 = False

    address_book_names_dict = {}
    address_book_groups_dict = {}
    for zone in self.addressbook:
      # building individual addresses dictionary
      groups = sorted(self.addressbook[zone])
      for group in groups:
        for address, name in self.addressbook[zone][group]:
          if name in address_book_names_dict:
            if address_book_names_dict[name].supernet_of(address):
              continue
          address_book_names_dict[name] = address

        # building individual address-group dictionary
        for nested_group in groups:
          group_names = []
          for address, name in self.addressbook[zone][nested_group]:
            group_names.append(name)
          address_book_groups_dict[nested_group] = group_names

      # sort address books and address sets
      address_book_groups_dict = collections.OrderedDict(
          sorted(address_book_groups_dict.items()))

    address_book_keys = sorted(
        list(address_book_names_dict.keys()), key=self._SortAddressBookNumCheck)

    # INITAL CONFIG
    config = etree.Element("config", {
        "version": "8.1.0",
        "urldb": "paloaltonetworks"
    })
    devices = etree.SubElement(config, "devices")
    device_entry = etree.SubElement(devices, "entry",
                                    {"name": "localhost.localdomain"})
    vsys = etree.SubElement(device_entry, "vsys")
    vsys_entry = etree.SubElement(vsys, "entry", {"name": "vsys1"})

    # APPLICATION
    app_entries = etree.Element("application")
    for app_name in self.applications:
      if app_name not in self.application_refs:
        # this is not a custom application.
        continue
      app = self.application_refs[app_name]
      app_entry = etree.SubElement(app_entries, "entry", {"name": app_name})
      for k in self.application_refs[app_name]:
        if isinstance(app[k], (str)):
          etree.SubElement(app_entry, k).text = app[k]
        elif isinstance(app[k], (dict)):
          if k == "default":
            default_props = etree.SubElement(app_entry, "default")
          else:
            continue
          for prop in app[k]:
            if k == "default" and prop in [
                "ident-by-icmp-type", "ident-by-icmp6-type"
            ]:
              icmp_type_props = etree.SubElement(default_props, prop)
              etree.SubElement(icmp_type_props, "type").text = app[k][prop]
            else:
              pass
    vsys_entry.append(app_entries)

    # APPLICATION GROUPS
    etree.SubElement(vsys_entry, "application-group")

    # SERVICES
    vsys_entry.append(etree.Comment(" Services "))
    service = etree.SubElement(vsys_entry, "service")
    for k, v in self.service_map.entries.items():
      entry = etree.SubElement(service, "entry", {"name": v["name"]})
      proto0 = etree.SubElement(entry, "protocol")
      proto = etree.SubElement(proto0, k[2])
      # destination port
      port = etree.SubElement(proto, "port")
      tup = str(k[1])[1:-1]
      if tup[-1] == ",":
        tup = tup[:-1]
      port.text = tup.replace("'", "").replace(", ", ",")
      # source port
      if len(k[0]):
        sport = etree.SubElement(proto, "source-port")
        tup = str(k[0])[1:-1]
        if tup[-1] == ",":
          tup = tup[:-1]
        sport.text = tup.replace("'", "").replace(", ", ",")

    # RULES
    vsys_entry.append(etree.Comment(" Rules "))
    rulebase = etree.SubElement(vsys_entry, "rulebase")
    security = etree.SubElement(rulebase, "security")
    rules = etree.SubElement(security, "rules")
    tag = etree.Element("tag")

    tag_num = 0

    # pytype: disable=key-error
    # pylint: disable=unused-variable
    for (header, pa_rules, filter_options) in self.pafw_policies:
      tag_name = None
      if header.comment:
        comment = " ".join(header.comment).strip()
        if comment:
          tag_num += 1
          # max tag len 127, max zone len 31
          tag_name = self._TAG_NAME_FORMAT.format(
              from_zone=filter_options[1],
              to_zone=filter_options[3],
              num=tag_num)
          tag_entry = etree.SubElement(tag, "entry", {"name": tag_name})
          comments = etree.SubElement(tag_entry, "comments")
          if len(comment) > self._MAX_TAG_COMMENTS_LENGTH:
            logging.warning(
                "WARNING: tag %s comments exceeds maximum "
                "length %d, truncated.", tag_name,
                self._MAX_TAG_COMMENTS_LENGTH)
          comments.text = comment[:self._MAX_TAG_COMMENTS_LENGTH]

      no_addr_obj = True if (len(filter_options) > 5 and
                             filter_options[5] == "no-addr-obj") else False

      for name, options in pa_rules.items():
        entry = etree.SubElement(rules, "entry", {"name": name})
        if options["description"]:
          descr = etree.SubElement(entry, "description")
          x = " ".join(options["description"])
          if len(x) > self._MAX_RULE_DESCRIPTION_LENGTH:
            logging.warning(
                "WARNING: rule %s description exceeds maximum "
                "length %d, truncated.", name,
                self._MAX_RULE_DESCRIPTION_LENGTH)
          descr.text = x[:self._MAX_RULE_DESCRIPTION_LENGTH]

        to = etree.SubElement(entry, "to")
        for x in options["to_zone"]:
          member = etree.SubElement(to, "member")
          member.text = x

        from_ = etree.SubElement(entry, "from")
        for x in options["from_zone"]:
          member = etree.SubElement(from_, "member")
          member.text = x

        af = filter_options[4] if len(filter_options) > 4 else "inet"

        max_src_dst = 0
        source = etree.SubElement(entry, "source")
        if not options["source"]:
          member = etree.SubElement(source, "member")
          if not options["destination"] and af != "mixed":
            # only inet and inet6 use the any-ipv4 object
            member.text = "any-ipv4"
            add_any_ipv4 = True
          else:
            member.text = "any"
        else:
          for x in options["source"]:
            if no_addr_obj:
              for group in address_book_groups_dict[x]:
                member = etree.SubElement(source, "member")
                member.text = str(address_book_names_dict[group])
                max_src_dst += 1
            else:
              member = etree.SubElement(source, "member")
              member.text = x
              max_src_dst += 1

        if max_src_dst > self._MAX_RULE_SRC_DST_MEMBERS:
          raise UnsupportedFilterError(
            "term %s source members exceeds maximum of %d: %d" %
            (name, self._MAX_RULE_SRC_DST_MEMBERS, max_src_dst))

        max_src_dst = 0
        dest = etree.SubElement(entry, "destination")
        if not options["destination"]:
          member = etree.SubElement(dest, "member")
          if options["source"]:
            member.text = "any"
          else:
            if af != "mixed":
              # only inet and inet6 use the any-ipv4 object
              member.text = "any-ipv4"
              if af == "inet6":
                for x in ["negate-source", "negate-destination"]:
                  negate = etree.SubElement(entry, x)
                  negate.text = "yes"
            else:
              member.text = "any"
        else:
          for x in options["destination"]:
            if no_addr_obj:
              for group in address_book_groups_dict[x]:
                member = etree.SubElement(dest, "member")
                member.text = str(address_book_names_dict[group])
                max_src_dst += 1
            else:
              member = etree.SubElement(dest, "member")
              member.text = x
              max_src_dst += 1

        if max_src_dst > self._MAX_RULE_SRC_DST_MEMBERS:
          raise UnsupportedFilterError(
            "term %s destination members exceeds maximum of %d: %d" %
            (name, self._MAX_RULE_SRC_DST_MEMBERS, max_src_dst))

        # service section of a policy rule.
        service = etree.SubElement(entry, "service")
        if not options["service"] and not options["application"]:
          member = etree.SubElement(service, "member")
          member.text = "any"
        elif not options["service"] and options["application"]:
          # Adds custom applications.
          member = etree.SubElement(service, "member")
          member.text = "application-default"
        else:
          # Adds services.
          for x in options["service"]:
            member = etree.SubElement(service, "member")
            member.text = x

        # ACTION
        action = etree.SubElement(entry, "action")
        action.text = options["action"]

        # check whether the rule is interzone
        if list(set(options["from_zone"]).difference(options["to_zone"])):
          type_ = etree.SubElement(entry, "rule-type")
          type_.text = "interzone"
        elif not options["from_zone"] and not options["to_zone"]:
          type_ = etree.SubElement(entry, "rule-type")
          type_.text = "interzone"

        # APPLICATION
        app = etree.SubElement(entry, "application")
        if not options["application"]:
          member = etree.SubElement(app, "member")
          member.text = "any"
        else:
          for x in options["application"]:
            member = etree.SubElement(app, "member")
            member.text = x

        if tag_name is not None:
          rules_tag = etree.SubElement(entry, "tag")
          member = etree.SubElement(rules_tag, "member")
          member.text = tag_name

        # LOGGING
        if options["logging"]:
          if "disable" in options["logging"]:
            log = etree.SubElement(entry, "log-start")
            log.text = "no"
            log = etree.SubElement(entry, "log-end")
            log.text = "no"
          if "log-start" in options["logging"]:
            log = etree.SubElement(entry, "log-start")
            log.text = "yes"
          if "log-end" in options["logging"]:
            log = etree.SubElement(entry, "log-end")
            log.text = "yes"

    # pytype: enable=key-error

    if no_addr_obj:
      address_book_groups_dict = {}
      address_book_keys = {}

    # ADDRESS
    vsys_entry.append(etree.Comment(" Address Groups "))
    addr_group = etree.SubElement(vsys_entry, "address-group")

    for group, address_list in address_book_groups_dict.items():
      entry = etree.SubElement(addr_group, "entry", {"name": group})
      static = etree.SubElement(entry, "static")
      for name in address_list:
        member = etree.SubElement(static, "member")
        member.text = name

    vsys_entry.append(etree.Comment(" Addresses "))
    addr = etree.SubElement(vsys_entry, "address")

    for name in address_book_keys:
      entry = etree.SubElement(addr, "entry", {"name": name})
      desc = etree.SubElement(entry, "description")
      desc.text = name
      ip = etree.SubElement(entry, "ip-netmask")
      ip.text = str(address_book_names_dict[name])

    if add_any_ipv4:
      entry = etree.SubElement(addr, "entry", {"name": "any-ipv4"})
      desc = etree.SubElement(entry, "description")
      desc.text = ("Object to match all IPv4 addresses; "
                   "negate to match all IPv6 addresses.")
      range = etree.SubElement(entry, "ip-range")
      range.text = ANY_IPV4_RANGE

    vsys_entry.append(tag)

    self.config = config
    document = etree.tostring(config, encoding="UTF-8")
    dom = minidom.parseString(document.decode("UTF-8"))

    return dom.toprettyxml(indent=self.INDENT)


# capirca/lib/pcap.py

# Copyright 2015 Google 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.
"""Pcap filter generator.

This generate a pcap packet filter expression that either:
  1) Matches (i.e., captures), the packets that match the ACCEPT clauses
     specified in a given policy, or
  2) Matches the packets that match opposite of that, i.e., the DENY or REJECT
     clauses.
Support tcp flags matching and icmptypes, including ipv6/icmpv6, but not much
else past the standard addres, port, and protocol conditions.

Note that this is still alpha and will likely require more testing prior to
having more confidence in it.

Stolen liberally from packetfilter.py.
"""

import datetime

from absl import logging
from capirca.lib import aclgenerator


class Error(Exception):
  """Base error class."""


class UnsupportedActionError(Error):
  """Raised when we see an unsupported action."""


class UnsupportedTargetOptionError(Error):
  """Raised when we see an unsupported option."""


class Term(aclgenerator.Term):
  """Generate pcap filter to match a policy term."""

  _PLATFORM = 'pcap'
  _ACTION_TABLE = {
      'accept': '',
      'deny': '',
      'reject': '',
      'next': '',
      }

  _TCP_FLAGS_TABLE = {
      'syn': 'tcp-syn',
      'ack': 'tcp-ack',
      'fin': 'tcp-fin',
      'rst': 'tcp-rst',
      'urg': 'tcp-urg',
      'psh': 'tcp-push',
      'all': '(tcp-syn|tcp-ack|tcp-fin|tcp-rst|tcp-urg|tcp-push)',
      'none': '(tcp-syn&tcp-ack&tcp-fin&tcp-rst&tcp-urg&tcp-push)',
      }

  _PROTO_TABLE = {
      'ah': 'proto \\ah',
      'esp': 'proto \\esp',
      'icmp': 'proto \\icmp',
      'icmpv6': 'icmp6',
      'ip': 'proto \\ip',
      'ip6': 'ip6',
      'igmp': 'proto \\igmp',
      'igrp': 'igrp',
      'pim': 'proto \\pim',
      'tcp': 'proto \\tcp',
      'udp': 'proto \\udp',
      # bpf supports "\vrrp", but some winpcap version dont' recognize it,
      # so use the IANA protocol number for it:
      'vrrp': 'proto 112',
      'hopopt': 'ip6 protochain 0',
      }

  def __init__(self, term, filter_name, af='inet', direction=''):
    """Setup a new term.

    Args:
      term: A policy.Term object to represent in packetfilter.
      filter_name: The name of the filter chan to attach the term to.
      af: Which address family ('inet' or 'inet6') to apply the term to.
      direction: Direction of the flow.

    Raises:
      aclgenerator.UnsupportedFilterError: Filter is not supported.
    """
    super().__init__(term)
    self.term = term  # term object
    self.filter = filter_name  # actual name of filter
    self.options = []
    self.default_action = 'deny'
    self.af = af
    self.direction = direction

  def __str__(self):
    """Render config output from this term object."""
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if self._PLATFORM not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if self._PLATFORM in self.term.platform_exclude:
        return ''

    conditions = []

    # if terms does not specify action, use filter default action
    if not self.term.action:
      self.term.action[0].value = self.default_action
    if str(self.term.action[0]) not in self._ACTION_TABLE:
      raise aclgenerator.UnsupportedFilterError('%s %s %s %s' % (
          '\n', self.term.name, self.term.action[0],
          'action not currently supported.'))

    # source address
    term_saddrs = self._CheckAddressAf(self.term.source_address)
    if not term_saddrs:
      logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                   direction='source',
                                                   af=self.af))
      return ''

    conditions.append(self._GenerateAddrStatement(
        term_saddrs, self.term.source_address_exclude))

    # destination address
    term_daddrs = self._CheckAddressAf(self.term.destination_address)
    if not term_daddrs:
      logging.debug(self.NO_AF_LOG_ADDR.substitute(term=self.term.name,
                                                   direction='destination',
                                                   af=self.af))
      return ''

    conditions.append(self._GenerateAddrStatement(
        term_daddrs, self.term.destination_address_exclude))

    # protocol
    if self.term.protocol_except:
      raise aclgenerator.UnsupportedFilterError('%s %s %s' % (
          '\n', self.term.name,
          'protocol_except logic not currently supported.'))
    conditions.append(self._GenerateProtoStatement(self.term.protocol))

    conditions.append(self._GeneratePortStatement(
        self.term.source_port, 'src'))
    conditions.append(self._GeneratePortStatement(
        self.term.destination_port, 'dst'))

    # icmp-type
    icmp_types = ['']
    if self.term.icmp_type:
      if self.term.protocol == ['icmp']:
        af = 'inet'
      elif self.term.protocol == ['icmpv6']:
        af = 'inet6'
      else:
        raise aclgenerator.UnsupportedFilterError(
            '%s %s %s' % ('\n', self.term.name,
                          'icmp protocol is not defined or not supported.'))
      icmp_types = self.NormalizeIcmpTypes(self.term.icmp_type,
                                           self.term.protocol, af)

      if 'icmp' in self.term.protocol:
        conditions.append(self._GenerateIcmpType(icmp_types,
                                                 self.term.icmp_code))

    # tcp options
    if 'tcp' in self.term.protocol:
      conditions.append(self._GenerateTcpOptions(self.term.option))

    cond = Term.JoinConditionals(conditions, 'and')

    # Note that directionally-based pcap filter requires post-processing to
    # replace 'localhost' with whatever the IP(s) of the local machine happen
    # to be.  This bit of logic ensure there's a placeholder with the
    # appropriate booleans around it.  We also have to check that there exists
    # some form of condition already, else we'll end up with something overly
    # broad like 'dst net localhost' (e.g., 'default-deny').
    if cond and self.direction == 'in':
      cond = Term.JoinConditionals(['dst net localhost', cond], 'and')
    elif cond and self.direction == 'out':
      cond = Term.JoinConditionals(['src net localhost', cond], 'and')

    return cond + '\n'

  def _CheckAddressAf(self, addrs):
    """Verify that the requested address-family matches the address's family."""
    if not addrs:
      return ['any']
    if self.af == 'mixed':
      return addrs
    af_addrs = []
    af = self.NormalizeAddressFamily(self.af)
    for addr in addrs:
      if addr.version == af:
        af_addrs.append(addr)
    return af_addrs

  @staticmethod
  def JoinConditionals(condition_list, operator):
    """Join conditionals using the specified operator.

    Filters out empty elements and blank strings.

    Args:
      condition_list:  a list of str()-able items to join.
      operator:  the join string.

    Returns:
      A string consisting of the joined elements.  If all elements are False
      or whitespace-only, the empty string.
    """
    condition_list = [_f for _f in condition_list if _f]
    condition_list = [str(x).strip(' ') for x in condition_list
                      if str(x).strip()]
    if not condition_list:
      return ''

    op = ' %s ' % (operator)
    res = '(%s)' % (op.join(condition_list))
    return res

  def _GenerateAddrStatement(self, addrs, exclude_addrs):
    addrlist = []
    for d in addrs:
      if d != 'any' and str(d) != '::/0':
        addrlist.append('dst net %s' % (d))

    excludes = []
    if exclude_addrs:
      for d in exclude_addrs:
        if d != 'any' and str(d) != '::/0':
          excludes.append('not dst net %s' % (d))
        else:
          # excluding 'any' doesn't really make sense ...
          return ''

    if excludes:
      return Term.JoinConditionals(
          [Term.JoinConditionals(addrlist, 'or'),
           Term.JoinConditionals(excludes, 'or')], 'and not')
    else:
      return Term.JoinConditionals(addrlist, 'or')

  def _GenerateProtoStatement(self, protocols):
    return Term.JoinConditionals(
        [self._PROTO_TABLE[p] for p in protocols], 'or')

  def _GeneratePortStatement(self, ports, direction):
    conditions = []
    # term.destination_port is a list of tuples containing the start and end
    # ports of the port range.  In the event it is a single port, the start
    # and end ports are the same.
    for port_tuple in ports:
      if port_tuple[0] == port_tuple[1]:
        conditions.append('%s port %s' % (direction, port_tuple[0]))
      else:
        conditions.append('%s portrange %s-%s' % (
            direction, port_tuple[0], port_tuple[1]))
    return Term.JoinConditionals(conditions, 'or')

  def _GenerateTcpOptions(self, options):
    opts = [str(x) for x in options]
    tcp_flags_set = []
    tcp_flags_check = []
    for next_opt in opts:
      if next_opt == 'tcp-established':
        tcp_flags_set.append(self._TCP_FLAGS_TABLE['ack'])
        tcp_flags_check.extend([self._TCP_FLAGS_TABLE['ack']])

      else:
        # Iterate through flags table, and create list of tcp-flags to append
        for next_flag in self._TCP_FLAGS_TABLE:
          if next_opt.find(next_flag) == 0:
            tcp_flags_check.append(self._TCP_FLAGS_TABLE.get(next_flag))
            tcp_flags_set.append(self._TCP_FLAGS_TABLE.get(next_flag))

    if tcp_flags_check:
      return '(tcp[tcpflags] & (%s) == (%s))' % ('|'.join(tcp_flags_check),
                                                 '|'.join(tcp_flags_set))
    return ''

  def _GenerateIcmpType(self, icmp_types, icmp_code):
    rtr_str = ''
    if icmp_types:
      code_strings = ['']
      if icmp_code:
        code_strings = [' and icmp[icmpcode] == %d' % code for
                        code in icmp_code]
      rtr_str = Term.JoinConditionals(
          ['icmp[icmptype] == %d%s' % (x, y) for y in code_strings for
           x in icmp_types], 'or')
    return rtr_str


class PcapFilter(aclgenerator.ACLGenerator):
  """Generates filters and terms from provided policy object.

  Note that since pcap isn't actually a firewall grammar, this generates a
  filter that only matches matches that which would be accepted by the
  specified policy.
  """

  _PLATFORM = 'pcap'
  _DEFAULT_PROTOCOL = 'all'
  SUFFIX = '.pcap'
  _TERM = Term

  def __init__(self, *args, **kwargs):
    """Initialize a PcapFilter generator.

    Takes standard ACLGenerator arguments, as well as an 'invert' kwarg.  If
    this argument is true, the pcap filter will be reversed, such that it
    matches all those packets that would be denied by the specified policy.

    Args:
      *args: Arguments.
      **kwargs: Keyword arguments.

    """
    self._invert = False
    if 'invert' in kwargs:
      self._invert = kwargs['invert']
      del kwargs['invert']
    super().__init__(*args, **kwargs)

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'logging', 'icmp_code'}
    supported_tokens -= {'verbatim'}

    supported_sub_tokens.update(
        {'action': {'accept', 'deny', 'reject', 'next'},
         'option': {
             'tcp-established',
             'established',
             'syn',
             'ack',
             'fin',
             'rst',
             'urg',
             'psh',
             'all',
             'none'},
        })

    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    self.pcap_policies = []
    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    good_afs = ['inet', 'inet6', 'mixed']
    good_options = ['in', 'out']
    direction = ''

    for header, terms in pol.filters:
      filter_type = None
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)[1:]
      filter_name = header.FilterName(self._PLATFORM)

      # ensure all options after the filter name are expected
      for opt in filter_options:
        if opt not in good_afs + good_options:
          raise UnsupportedTargetOptionError('%s %s %s %s' % (
              '\nUnsupported option found in', self._PLATFORM,
              'target definition:', opt))

      if 'in' in filter_options:
        direction = 'in'
      elif 'out' in filter_options:
        direction = 'out'

      # Check for matching af
      for address_family in good_afs:
        if address_family in filter_options:
          # should not specify more than one AF in options
          if filter_type is not None:
            raise aclgenerator.UnsupportedFilterError('%s %s %s %s' % (
                '\nMay only specify one of', good_afs, 'in filter options:',
                filter_options))
          filter_type = address_family
      if filter_type is None:
        filter_type = 'mixed'

      # add the terms
      accept_terms = []
      deny_terms = []
      term_names = set()
      for term in terms:
        if term.name in term_names:
          raise aclgenerator.DuplicateTermError(
              'You have a duplicate term: %s' % term.name)

        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue

        if not term:
          continue

        if term.action[0] == 'accept':
          accept_terms.append(self._TERM(term, filter_name, filter_type,
                                         direction))
        elif term.action[0] == 'deny' or term.action[0] == 'reject':
          deny_terms.append(self._TERM(term, filter_name, filter_type,
                                       direction))

      self.pcap_policies.append((header, filter_name, filter_type, accept_terms,
                                 deny_terms))

  def __str__(self):
    """Render the output of the PF policy into config."""
    target = []

    for (unused_header, unused_filter_name, unused_filter_type, accept_terms,
         deny_terms) in self.pcap_policies:

      accept = []
      for term in accept_terms:
        term_str = str(term)
        if term_str:
          accept.append(str(term))
      accept_clause = Term.JoinConditionals(accept, 'and')

      deny = []
      for term in deny_terms:
        term_str = str(term)
        if term_str:
          deny.append(str(term))
      deny_clause = Term.JoinConditionals(deny, 'and')

      if self._invert:
        target.append(
            Term.JoinConditionals([deny_clause, accept_clause], 'and not'))
      else:
        target.append(
            Term.JoinConditionals([accept_clause, deny_clause], 'and not'))

    return '\nor\n'.join(target) + '\n'


# capirca/lib/policy.py

# Copyright 2011 Google 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.
#

"""Parses the generic policy files and return a policy object for acl rendering.
"""

import datetime
import os
import sys

from absl import logging
from capirca.lib import nacaddr
from capirca.lib import naming
from ply import lex
from ply import yacc


DEFINITIONS = None
DEFAULT_DEFINITIONS = './def'
ACTIONS = set(('accept', 'count', 'deny', 'reject', 'next',
               'reject-with-tcp-rst'))
PROTOS_WITH_PORTS = frozenset(('tcp', 'udp', 'udplite', 'sctp'))
_FLEXIBLE_MATCH_RANGE_ATTRIBUTES = {'byte-offset',
                                    'bit-offset',
                                    'bit-length',
                                    'match-start',
                                    'range',
                                    'range-except',
                                    'flexible-range-name'}
_FLEXIBLE_MATCH_START_OPTIONS = {'layer-3', 'layer-4', 'payload'}
_LOGGING = set(('true', 'True', 'syslog', 'local', 'disable', 'log-both'))
_OPTIMIZE = True
_SHADE_CHECK = False
_MAX_TTL = 255
_MIN_TTL = 0


class Error(Exception):
  """Generic error class."""


class FileNotFoundError(Error):
  """Policy file unable to be read."""


class FileReadError(Error):
  """Policy file unable to be read."""


class RecursionTooDeepError(Error):
  """Included files exceed maximum recursion depth."""


class ParseError(Error):
  """ParseError in the input."""


class TermAddressExclusionError(Error):
  """Excluded address block is not contained in the accepted address block."""


class TermObjectTypeError(Error):
  """Error with an object passed to Term."""


class TermPortProtocolError(Error):
  """Error when a requested protocol doesn't have any of the requested ports."""


class TermProtocolEtherTypeError(Error):
  """Error when both ether-type & upper-layer protocol matches are requested."""


class TermNoActionError(Error):
  """Error when a term hasn't defined an action."""


class TermInvalidIcmpType(Error):
  """Error when a term has invalid icmp-types specified."""


class InvalidTermActionError(Error):
  """Error when an action is invalid."""


class InvalidTermLoggingError(Error):
  """Error when a option is set for logging."""


class UndefinedAddressError(Error):
  """Error when an undefined address is referenced."""


class NoTermsError(Error):
  """Error when no terms were found."""


class ShadingError(Error):
  """Error when a term is shaded by a prior term."""


class FlexibleMatchError(Error):
  """Error when a term contains an invalid flexible match value."""


class ICMPCodeError(Error):
  """Error when ICMP Codes are used with multiple or invalid types."""


class InvalidTermTTLValue(Error):
  """Error when TTL value is invalid."""


class MixedPortandNonPortProtos(Error):
  """Error when protocols that use ports are mixed with protocols that do not"""


def TranslatePorts(ports, protocols, term_name):
  """Return all ports of all protocols requested.

  Args:
    ports: list of ports, eg ['SMTP', 'DNS', 'HIGH_PORTS']
    protocols: list of protocols, eg ['tcp', 'udp']
    term_name: name of current term, used for warning messages

  Returns:
    ret_array: list of ports tuples such as [(25,25), (53,53), (1024,65535)]

  Note:
    Duplication will be taken care of in Term.CollapsePortList
  """
  ret_array = []
  for proto in protocols:
    for port in ports:
      service_by_proto = DEFINITIONS.GetServiceByProto(port, proto)
      if not service_by_proto:
        logging.warning('Term %s has service %s which is not defined with '
                        'protocol %s, but will be permitted. Unless intended'
                        ', you should consider splitting the protocols '
                        'into separate terms!', term_name, port, proto)

      for p in [x.split('-') for x in service_by_proto]:
        if len(p) == 1:
          ret_array.append((int(p[0]), int(p[0])))
        else:
          ret_array.append((int(p[0]), int(p[1])))
  return ret_array


# classes for storing the object types in the policy files.
class Policy:
  """The policy object contains everything found in a given policy file."""

  def __init__(self, header, terms):
    """Initiator for the Policy object.

    Args:
      header: __main__.Header object. contains comments which should be passed
        on to the rendered acls as well as the type of acls this policy file
        should render to.

      terms: list __main__.Term. an array of Term objects which must be rendered
        in each of the rendered acls.

    Attributes:
      filters: list of tuples containing (header, terms).
    """
    self.filters = []
    self.filename = ''
    self.AddFilter(header, terms)

  def AddFilter(self, header, terms):
    """Add another header & filter."""
    self.filters.append((header, terms))
    self._TranslateTerms(terms)
    if _SHADE_CHECK:
      self._DetectShading(terms)

  def _TranslateTerms(self, terms):
    """."""
    if not terms:
      raise NoTermsError('no terms found')
    for term in terms:
      # TODO(pmoody): this probably belongs in Term.SanityCheck(),
      # or at the very least, in some method under class Term()
      if term.translated:
        continue
      if term.port:
        term.port = TranslatePorts(term.port, term.protocol, term.name)
        if not term.port:
          raise TermPortProtocolError(
              'no ports of the correct protocol for term %s' % (
                  term.name))
      if term.source_port:
        term.source_port = TranslatePorts(term.source_port, term.protocol,
                                          term.name)
        if not term.source_port:
          raise TermPortProtocolError(
              'no source ports of the correct protocol for term %s' % (
                  term.name))
      if term.destination_port:
        term.destination_port = TranslatePorts(term.destination_port,
                                               term.protocol, term.name)
        if not term.destination_port:
          raise TermPortProtocolError(
              'no destination ports of the correct protocol for term %s' % (
                  term.name))

      # If argument is true, we optimize, otherwise just sort addresses
      term.AddressCleanup(_OPTIMIZE, self._NeedsAddressBook())
      term.SanityCheck()
      term.translated = True

  def _NeedsAddressBook(self):
    """Returns True if the policy uses a generator needing an addressbook."""
    for header in self.headers:
      if not header:
        continue
      if 'srx' in header.platforms:
        return True
      for target in header.target:
        opts = header.FilterOptions(target.platform)
        if opts and 'object-group' in opts:
          return True
    return False

  @property
  def headers(self):
    """Returns the headers from each of the configured filters.

    Returns:
      headers
    """
    return [x[0] for x in self.filters]

  def _DetectShading(self, terms):
    """Finds terms which are shaded (impossible to reach).

    Iterate through each term, looking at each prior term. If a prior term
    contains every component of the current term then the current term would
    never be hit and is thus shaded. This can be a mistake.

    Args:
      terms: list of Term objects.

    Raises:
      ShadingError: When a term is impossible to reach.
    """
    shading_errors = []
    for index, term in enumerate(terms):
      for prior_index in range(index):
        # Check each term that came before for shading. Terms with next as an
        # action do not terminate evaluation, so cannot shade.
        if (term in terms[prior_index]
            and 'next' not in terms[prior_index].action):
          shading_errors.append(
              '  %s is shaded by %s.' % (
                  term.name, terms[prior_index].name))
    if shading_errors:
      raise ShadingError('\n'.join(shading_errors))

  def __eq__(self, obj):
    """Compares for equality against another Policy object.

    Note that it is picky and requires the list contents to be in the
    same order.

    Args:
      obj: object to be compared to for equality.
    Returns:
      True if the list of filters in this policy object is equal to the list
      in obj and False otherwise.
    """
    if not isinstance(obj, Policy):
      return False
    return self.filters == obj.filters

  def __str__(self):
    def tuple_str(tup):
      return '%s:%s' % (tup[0], tup[1])
    return 'Policy: {%s}' % ', '.join(map(tuple_str, self.filters))

  def __repr__(self):
    return self.__str__()


class Term:
  """The Term object is used to store each of the terms.

  Args:
    obj: an object of type VarType or a list of objects of type VarType

  members:
    address/source_address/destination_address/: list of
      VarType.(S|D)?ADDRESS's
    address_exclude/source_address_exclude/destination_address_exclude: list of
      VarType.(S|D)?ADDEXCLUDE's
    restrict-address-family: VarType.RESTRICT_ADDRESS_FAMILY
    port/source_port/destination_port: list of VarType.(S|D)?PORT's
    options: list of VarType.OPTION's.
    protocol: list of VarType.PROTOCOL's.
    counter: VarType.COUNTER
    traffic-class-count: VarType.TRAFFIC_CLASS_COUNT
    action: list of VarType.ACTION's
    dscp-set: VarType.DSCP_SET
    dscp-match: VarType.DSCP_MATCH
    dscp-except: VarType.DSCP_EXCEPT
    comments: VarType.COMMENT
    encapsulate: VarType.ENCAPSULATE
    filter-term: VarType.FILTER_TERM
    flexible-match-range: VarType.FLEXIBLE_MATCH_RANGE
    forwarding-class: VarType.FORWARDING_CLASS
    forwarding-class-except: VarType.FORWARDING_CLASS_EXCEPT
    expiration: VarType.EXPIRATION
    verbatim: VarType.VERBATIM
    logging: VarType.LOGGING
    log_name: VarType.LOG_NAME
    next-ip: VarType.NEXT_IP
    qos: VarType.QOS
    pan-application: VarType.PAN_APPLICATION
    policer: VarType.POLICER
    priority: VarType.PRIORITY
    vpn: VarType.VPN
  """
  ICMP_TYPE = {4: {'echo-reply': 0,
                   'unreachable': 3,
                   'source-quench': 4,
                   'redirect': 5,
                   'alternate-address': 6,
                   'echo-request': 8,
                   'router-advertisement': 9,
                   'router-solicitation': 10,
                   'time-exceeded': 11,
                   'parameter-problem': 12,
                   'timestamp-request': 13,
                   'timestamp-reply': 14,
                   'information-request': 15,
                   'information-reply': 16,
                   'mask-request': 17,
                   'mask-reply': 18,
                   'conversion-error': 31,
                   'mobile-redirect': 32,
                  },
               6: {'destination-unreachable': 1,
                   'packet-too-big': 2,
                   'time-exceeded': 3,
                   'parameter-problem': 4,
                   'echo-request': 128,
                   'echo-reply': 129,
                   'multicast-listener-query': 130,
                   'multicast-listener-report': 131,
                   'multicast-listener-done': 132,
                   'router-solicit': 133,
                   'router-advertisement': 134,
                   'neighbor-solicit': 135,
                   'neighbor-advertisement': 136,
                   'redirect-message': 137,
                   'router-renumbering': 138,
                   'icmp-node-information-query': 139,
                   'icmp-node-information-response': 140,
                   'inverse-neighbor-discovery-solicitation': 141,
                   'inverse-neighbor-discovery-advertisement': 142,
                   'version-2-multicast-listener-report': 143,
                   'home-agent-address-discovery-request': 144,
                   'home-agent-address-discovery-reply': 145,
                   'mobile-prefix-solicitation': 146,
                   'mobile-prefix-advertisement': 147,
                   'certification-path-solicitation': 148,
                   'certification-path-advertisement': 149,
                   'multicast-router-advertisement': 151,
                   'multicast-router-solicitation': 152,
                   'multicast-router-termination': 153,
                  },
              }
  ICMP_CODE = {'unreachable': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
                               10, 11, 12, 13, 14, 15],
               'redirect': [0, 1, 2, 3],
               'router-advertisement': [0, 16],
               'time-exceeded': [0, 1],
               'parameter-problem': [0, 1, 2],
               'destination-unreachable': [0, 1, 2, 3, 4, 5, 6, 7],
               'parameter-problem': [0, 1, 2, 3],
               'router-renumbering': [0, 1, 255],
               'icmp-node-information-query': [0, 1, 2],
               'icmp-node-information-response': [0, 1, 2],
              }
  _IPV4_BYTE_SIZE = 1
  _IPV6_BYTE_SIZE = 4

  def __init__(self, obj):
    self.name = None

    self.action = []
    self.address = []
    self.address_exclude = []
    self.restrict_address_family = None
    self.comment = []
    self.counter = None
    self.expiration = None
    self.destination_address = []
    self.destination_address_exclude = []
    self.destination_port = []
    self.destination_prefix = []
    self.filter_term = None
    self.forwarding_class = []
    self.forwarding_class_except = []
    self.logging = []
    self.log_limit = None
    self.log_name = None
    self.loss_priority = None
    self.option = []
    self.owner = None
    self.policer = None
    self.port = []
    self.precedence = []
    self.protocol = []
    self.protocol_except = []
    self.qos = None
    self.pan_application = []
    self.routing_instance = None
    self.source_address = []
    self.source_address_exclude = []
    self.source_port = []
    self.source_prefix = []
    self.ttl = None
    self.verbatim = []
    # juniper specific.
    self.packet_length = None
    self.fragment_offset = None
    self.hop_limit = None
    self.icmp_type = []
    self.icmp_code = []
    self.ether_type = []
    self.traffic_class_count = None
    self.traffic_type = []
    self.translated = False
    self.dscp_set = None
    self.dscp_match = []
    self.dscp_except = []
    self.next_ip = None
    self.flexible_match_range = []
    self.source_prefix_except = []
    self.destination_prefix_except = []
    self.inactive = False
    self.encapsulate = None
    # srx specific
    self.vpn = None
    # gce specific
    self.source_tag = []
    self.destination_tag = []
    self.priority = None
    # iptables specific
    self.source_interface = None
    self.destination_interface = None
    self.platform = []
    self.platform_exclude = []
    self.target_resources = []
    self.target_service_accounts = []
    self.timeout = None
    self.flattened = False
    self.flattened_addr = None
    self.flattened_saddr = None
    self.flattened_daddr = None
    self.stateless_reply = False

    # AddObject touches variables which might not have been initialized
    # further up so this has to be at the end.
    self.AddObject(obj)

  def __contains__(self, other):
    """Determine if other term is contained in this term."""
    if self.verbatim or other.verbatim:
      # short circuit these
      if sorted(list(self.verbatim)) != sorted(other.verbatim):
        return False

    # check protocols
    # either protocol or protocol-except may be used, not both at the same time.
    if self.protocol:
      if other.protocol:
        if not self.CheckProtocolIsContained(other.protocol, self.protocol):
          return False
      # this term has protocol, other has protocol_except.
      elif other.protocol_except:
        return False
      else:
        # other does not have protocol or protocol_except. since we do other
        # cannot be contained in self.
        return False
    elif self.protocol_except:
      if other.protocol_except:
        if not self.CheckProtocolIsContained(
            self.protocol_except, other.protocol_except):
          return False
      elif other.protocol:
        for proto in other.protocol:
          if proto in self.protocol_except:
            return False
      else:
        return False

    # combine addresses with exclusions for proper contains comparisons.
    if not self.flattened:
      self.FlattenAll()
    if not other.flattened:
      other.FlattenAll()

    # flat 'address' is compared against other flat (saddr|daddr).
    # if NONE of these evaluate to True other is not contained.
    if not (
        self.CheckAddressIsContained(
            self.flattened_addr, other.flattened_addr)
        or self.CheckAddressIsContained(
            self.flattened_addr, other.flattened_saddr)
        or self.CheckAddressIsContained(
            self.flattened_addr, other.flattened_daddr)):
      return False

    # compare flat address from other to flattened self (saddr|daddr).
    if not (
        # other's flat address needs both self saddr & daddr to contain in order
        # for the term to be contained. We already compared the flattened_addr
        # attributes of both above, which was not contained.
        self.CheckAddressIsContained(
            other.flattened_addr, self.flattened_saddr)
        and self.CheckAddressIsContained(
            other.flattened_addr, self.flattened_daddr)):
      return False

    # basic saddr/daddr check.
    if not (
        self.CheckAddressIsContained(
            self.flattened_saddr, other.flattened_saddr)):
      return False
    if not (
        self.CheckAddressIsContained(
            self.flattened_daddr, other.flattened_daddr)):
      return False

    # check ports
    # like the address directive, the port directive is special in that it can
    # be either source or destination.
    if self.port:
      if not (self.CheckPortIsContained(self.port, other.port) or
              self.CheckPortIsContained(self.port, other.source_port) or
              self.CheckPortIsContained(self.port, other.destination_port)):
        return False
    if not self.CheckPortIsContained(self.source_port, other.source_port):
      return False
    if not self.CheckPortIsContained(self.destination_port,
                                     other.destination_port):
      return False

    # prefix lists
    if self.source_prefix:
      if sorted(self.source_prefix) != sorted(other.source_prefix):
        return False
    if self.source_prefix_except:
      if sorted(self.source_prefix_except) != sorted(
          other.source_prefix_except):
        return False
    if self.destination_prefix:
      if sorted(self.destination_prefix) != sorted(
          other.destination_prefix):
        return False
    if self.destination_prefix_except:
      if sorted(self.destination_prefix_except) != sorted(
          other.destination_prefix_except):
        return False

    # check source and destination tags
    if self.source_tag:
      if sorted(self.source_tag) != sorted(other.source_tag):
        return False
      if sorted(self.destination_tag) != sorted(other.destination_tag):
        return False

    # check precedence
    if self.precedence:
      if not other.precedence:
        return False
      for precedence in other.precedence:
        if precedence not in self.precedence:
          return False
    elif other.precedence:
      return False
    # check various options
    if self.option:
      if not other.option:
        return False
      for opt in other.option:
        if opt not in self.option:
          return False
    elif other.option:
      return False
    # check forwarding-class
    if self.forwarding_class:
      if not other.forwarding_class:
        return False
      for fc in other.forwarding_class:
        if fc not in self.forwarding_class:
          return False
    # check forwarding-class-except
    if self.forwarding_class_except:
      if not other.forwarding_class_except:
        return False
      for fc in other.forwarding_class_except:
        if fc not in self.forwarding_class_except:
          return False
    if self.next_ip:
      if not other.next_ip:
        return False
    if self.encapsulate:
      if not other.encapsulate:
        return False
    if self.fragment_offset:
      # fragment_offset looks like 'integer-integer' or just, 'integer'
      sfo = sorted([int(x) for x in self.fragment_offset.split('-')])
      if other.fragment_offset:
        ofo = sorted([int(x) for x in other.fragment_offset.split('-')])
        if ofo[0] < sfo[0] or sfo[1:] < ofo[1:]:
          return False
      else:
        return False
    if self.hop_limit:
      # hop_limit looks like 'integer-integer' or just, 'integer'
      shl = [int(x) for x in self.hop_limit.split('-')]
      if other.hop_limit:
        ohl = [int(x) for x in other.hop_limit.split('-')]
        if shl[0] < ohl[0]:
          return False
        shll, ohll = shl[1:2], ohl[1:2]
        if shll and ohll:
          if shl[0] > ohl[0]:
            return False
      else:
        return False
    if self.packet_length:
      # packet_length looks like 'integer-integer' or just, 'integer'
      spl = [int(x) for x in self.packet_length.split('-')]
      if other.packet_length:
        opl = [int(x) for x in other.packet_length.split('-')]
        if spl[0] < opl[0] or sorted(spl[1:]) > sorted(opl[1:]):
          return False
      else:
        return False
    if self.icmp_type:
      if sorted(self.icmp_type) is not sorted(other.icmp_type):
        return False

    if self.icmp_code:
      if sorted(self.icmp_code) is not sorted(other.icmp_code):
        return False

    # check platform
    if self.platform:
      if sorted(self.platform) is not sorted(other.platform):
        return False
    if self.platform_exclude:
      if sorted(self.platform_exclude) is not sorted(other.platform_exclude):
        return False

    # we have containment
    return True

  def __str__(self):
    ret_str = []
    ret_str.append(' name: %s' % self.name)
    if self.address:
      ret_str.append('  address: %s' % sorted(self.address))
    if self.address_exclude:
      ret_str.append('  address_exclude: %s' % sorted(self.address_exclude))
    if self.source_address:
      ret_str.append('  source_address: %s' %
                     self._SortAddressesByFamily('source_address'))
    if self.source_address_exclude:
      ret_str.append('  source_address_exclude: %s' %
                     self._SortAddressesByFamily('source_address_exclude'))
    if self.source_tag:
      ret_str.append('  source_tag: %s' % self.source_tag)
    if self.destination_address:
      ret_str.append('  destination_address: %s' %
                     self._SortAddressesByFamily('destination_address'))
    if self.destination_address_exclude:
      ret_str.append('  destination_address_exclude: %s' %
                     self._SortAddressesByFamily('destination_address_exclude'))
    if self.destination_tag:
      ret_str.append('  destination_tag: %s' % self.destination_tag)
    if self.target_resources:
      ret_str.append('  target_resources: %s' % self.target_resources)
    if self.target_service_accounts:
      ret_str.append('  target_service_accounts: %s' %
                     self.target_service_accounts)
    if self.source_prefix:
      ret_str.append('  source_prefix: %s' % self.source_prefix)
    if self.source_prefix_except:
      ret_str.append('  source_prefix_except: %s' % self.source_prefix_except)
    if self.destination_prefix:
      ret_str.append('  destination_prefix: %s' % self.destination_prefix)
    if self.destination_prefix_except:
      ret_str.append('  destination_prefix_except: %s' %
                     self.destination_prefix_except)
    if self.filter_term:
      ret_str.append('  filter_term: %s' % self.filter_term)
    if self.forwarding_class:
      ret_str.append('  forwarding_class: %s' % self.forwarding_class)
    if self.forwarding_class_except:
      ret_str.append('  forwarding_class_except: %s'
                     % self.forwarding_class_except)
    if self.icmp_type:
      ret_str.append('  icmp_type: %s' % sorted(self.icmp_type))
    if self.icmp_code:
      ret_str.append('  icmp_code: %s' % sorted(self.icmp_code))
    if self.next_ip:
      ret_str.append('  next_ip: %s' % self.next_ip)
    if self.encapsulate:
      ret_str.append('  encapsulate: %s' % self.encapsulate)
    if self.protocol:
      ret_str.append('  protocol: %s' % sorted(self.protocol))
    if self.protocol_except:
      ret_str.append('  protocol-except: %s' % self.protocol_except)
    if self.owner:
      ret_str.append('  owner: %s' % self.owner)
    if self.port:
      ret_str.append('  port: %s' % sorted(self.port))
    if self.source_port:
      ret_str.append('  source_port: %s' % sorted(self.source_port))
    if self.destination_port:
      ret_str.append('  destination_port: %s' % sorted(self.destination_port))
    if self.action:
      ret_str.append('  action: %s' % self.action)
    if self.option:
      ret_str.append('  option: %s' % self.option)
    if self.flexible_match_range:
      ret_str.append('  flexible_match_range: %s' % self.flexible_match_range)
    if self.qos:
      ret_str.append('  qos: %s' % self.qos)
    if self.pan_application:
      ret_str.append('  pan_application: %s' % self.pan_application)
    if self.logging:
      ret_str.append('  logging: %s' % self.logging)
    if self.log_limit:
      ret_str.append('  log_limit: %s/%s' % (self.log_limit[0],
                                             self.log_limit[1]))
    if self.log_name:
      ret_str.append('  log_name: %s' % self.log_name)
    if self.priority:
      ret_str.append('  priority: %s' % self.priority)
    if self.counter:
      ret_str.append('  counter: %s' % self.counter)
    if self.traffic_class_count:
      ret_str.append('  traffic_class_count: %s' % self.traffic_class_count)
    if self.source_interface:
      ret_str.append('  source_interface: %s' % self.source_interface)
    if self.destination_interface:
      ret_str.append('  destination_interface: %s' % self.destination_interface)
    if self.expiration:
      ret_str.append('  expiration: %s' % self.expiration)
    if self.platform:
      ret_str.append('  platform: %s' % self.platform)
    if self.platform_exclude:
      ret_str.append('  platform_exclude: %s' % self.platform_exclude)
    if self.ttl:
      ret_str.append('  ttl: %s' % self.ttl)
    if self.timeout:
      ret_str.append('  timeout: %s' % self.timeout)
    if self.vpn:
      vpn_name, pair_policy = self.vpn
      if pair_policy:
        ret_str.append('  vpn: name = %s, pair_policy = %s' %
                       (vpn_name, pair_policy))
      else:
        ret_str.append('  vpn: name = %s' % vpn_name)

    return '\n'.join(ret_str)

  def __repr__(self):
    return self.__str__()

  def __eq__(self, other):
    # action
    if sorted(self.action) != sorted(other.action):
      return False

    # addresses.
    if not (sorted(self.address) == sorted(other.address) and
            sorted(self.source_address) == sorted(other.source_address) and
            sorted(self.source_address_exclude) ==
            sorted(other.source_address_exclude) and
            sorted(self.destination_address) ==
            sorted(other.destination_address) and
            sorted(self.destination_address_exclude) ==
            sorted(other.destination_address_exclude)):
      return False

    # prefix lists
    if not (sorted(self.source_prefix) == sorted(other.source_prefix) and
            sorted(self.source_prefix_except) ==
            sorted(other.source_prefix_except) and
            sorted(self.destination_prefix) ==
            sorted(other.destination_prefix) and
            sorted(self.destination_prefix_except) ==
            sorted(other.destination_prefix_except)):
      return False

    # ports
    if not (sorted(self.port) == sorted(other.port) and
            sorted(self.source_port) == sorted(other.source_port) and
            sorted(self.destination_port) == sorted(other.destination_port)):
      return False

    # protocol
    if not (sorted(self.protocol) == sorted(other.protocol) and
            sorted(self.protocol_except) == sorted(other.protocol_except)):
      return False

    # option
    if sorted(self.option) != sorted(other.option):
      return False

    # qos
    if self.qos != other.qos:
      return False

    # pan-application
    if sorted(self.pan_application) != sorted(other.pan_application):
      return False

    # verbatim
    if self.verbatim != other.verbatim:
      return False

    # policer
    if self.policer != other.policer:
      return False

    # interface
    if self.source_interface != other.source_interface:
      return False

    if self.destination_interface != other.destination_interface:
      return False

    # tags
    if not (sorted(self.source_tag) == sorted(other.source_tag) and
            sorted(self.destination_tag) == sorted(other.destination_tag)):
      return False

    if self.ttl != other.ttl:
      return False

    if sorted(self.logging) != sorted(other.logging):
      return False
    if self.log_limit != other.log_limit:
      return False
    if self.qos != other.qos:
      return False
    if sorted(self.pan_application) != sorted(other.pan_application):
      return False
    if self.packet_length != other.packet_length:
      return False
    if self.fragment_offset != other.fragment_offset:
      return False
    if self.hop_limit != other.hop_limit:
      return False
    if sorted(self.icmp_type) != sorted(other.icmp_type):
      return False
    if sorted(self.icmp_code) != sorted(other.icmp_code):
      return False
    if sorted(self.ether_type) != sorted(other.ether_type):
      return False
    if sorted(self.traffic_type) != sorted(other.traffic_type):
      return False

    # vpn
    if self.vpn != other.vpn:
      return False

    # platform
    if not (sorted(self.platform) == sorted(other.platform) and
            sorted(self.platform_exclude) == sorted(other.platform_exclude)):
      return False

    # timeout
    if self.timeout != other.timeout:
      return False

    # precedence
    if self.precedence != other.precedence:
      return False

    # filter
    if self.filter_term != other.filter_term:
      return False

    # forwarding-class
    if sorted(self.forwarding_class) != sorted(other.forwarding_class):
      return False

    # forwarding-class-except
    if sorted(self.forwarding_class_except) != sorted(
        other.forwarding_class_except):
      return False

    # next_ip
    if self.next_ip != other.next_ip:
      return False

    # encapsulate
    if self.encapsulate != other.encapsulate:
      return False

    # flexible_match
    if self.flexible_match_range != other.flexible_match_range:
      return False

    return True

  def __ne__(self, other):
    return not self.__eq__(other)

  def _SortAddressesByFamily(self, addr_type):
    """Provide the term address field to sort.

    Method will sort v4 and then concatenate sorted v6 addresses. This will
    support Term.__str__ function which outputs a string of
    sorted IP addresses.

    Args:
      addr_type: string, this will be either 'source_address',
        'source_address_exclude', 'destination_address' or
        'destination_address_exclude'
    Returns:
      List of IP addresses sourted v4 then v6
    """
    # Sort v4 and v6
    sort_v4 = sorted(self.GetAddressOfVersion(addr_type, 4))
    sort_v6 = sorted(self.GetAddressOfVersion(addr_type, 6))

    # Concatenate
    return sort_v4 + sort_v6

  def AddressesByteLength(self, address_family=(4, 6)):
    """Returns the byte length of all IP addresses in the term.

    This is used in the srx generator due to a address size limitation.

    Args:
      address_family: Address families to include for determining byte length.

    Returns:
      counter: Byte length of the sum of both source and destination IPs.
    """
    counter = 0
    for i in self.source_address:
      if i.version == 6 and i.version in address_family:
        counter += self._IPV6_BYTE_SIZE
      elif i.version == 4 and i.version in address_family:
        counter += self._IPV4_BYTE_SIZE
    for i in self.destination_address:
      if i.version == 6 and i.version in address_family:
        counter += self._IPV6_BYTE_SIZE
      elif i.version == 4 and i.version in address_family:
        counter += self._IPV4_BYTE_SIZE
    return counter

  def FlattenAll(self):
    """Reduce source, dest, and address fields to their post-exclude state.

    Populates the self.flattened_addr, self.flattened_saddr,
    self.flattened_daddr by removing excludes from includes.
    """
    # No excludes, set flattened attributes and move along.
    self.flattened = True
    if not (self.source_address_exclude or self.destination_address_exclude or
            self.address_exclude):
      self.flattened_saddr = self.source_address
      self.flattened_daddr = self.destination_address
      self.flattened_addr = self.address
      return

    if self.source_address_exclude:
      self.flattened_saddr = nacaddr.AddressListExclude(
          self.source_address,
          self.source_address_exclude,
          collapse_addrs=False)
      self.source_address = self.flattened_saddr
    if self.destination_address_exclude:
      self.flattened_daddr = nacaddr.AddressListExclude(
          self.destination_address,
          self.destination_address_exclude,
          collapse_addrs=False)
      self.destination_address = self.flattened_daddr
    if self.address_exclude:
      self.flattened_addr = nacaddr.AddressListExclude(
          self.address, self.address_exclude, collapse_addrs=False)
      self.address = self.flattened_addr

  def GetAddressOfVersion(self, addr_type, af=None):
    """Returns addresses of the appropriate Address Family.

    Args:
      addr_type: string, this will be either
        'source_address', 'source_address_exclude',
        'destination_address' or 'destination_address_exclude'
      af: int or None, either 4 for IPv4 or 6 for IPv6

    Returns:
      list of addresses of the correct family.
    """
    if not af:
      return getattr(self, addr_type)

    return [x for x in getattr(self, addr_type) if x.version == af]

  def AddObject(self, obj):
    """Add an object of unknown type to this term.

    Args:
      obj: single or list of either
        [Address, Port, Option, Protocol, Counter, Action, Comment, Expiration]

    Raises:
      InvalidTermActionError: if the action defined isn't an accepted action.
        eg, action:: godofoobar
      TermObjectTypeError: if AddObject is called with an object it doesn't
        understand.
      InvalidTermLoggingError: when a option is set for logging not known.
    """
    if type(obj) is list:
      for x in obj:
        # do we have a list of addresses?
        # expanded address fields consolidate naked address fields with
        # saddr/daddr.
        if x.var_type is VarType.SADDRESS:
          saddr = DEFINITIONS.GetNetAddr(x.value)
          self.source_address.extend(saddr)
        elif x.var_type is VarType.DADDRESS:
          daddr = DEFINITIONS.GetNetAddr(x.value)
          self.destination_address.extend(daddr)
        elif x.var_type is VarType.ADDRESS:
          addr = DEFINITIONS.GetNetAddr(x.value)
          self.address.extend(addr)
        # do we have address excludes?
        elif x.var_type is VarType.SADDREXCLUDE:
          saddr_exclude = DEFINITIONS.GetNetAddr(x.value)
          self.source_address_exclude.extend(saddr_exclude)
        elif x.var_type is VarType.DADDREXCLUDE:
          daddr_exclude = DEFINITIONS.GetNetAddr(x.value)
          self.destination_address_exclude.extend(daddr_exclude)
        elif x.var_type is VarType.ADDREXCLUDE:
          addr_exclude = DEFINITIONS.GetNetAddr(x.value)
          self.address_exclude.extend(addr_exclude)
        # do we have a list of ports?
        elif x.var_type is VarType.PORT:
          self.port.append(x.value)
        elif x.var_type is VarType.SPORT:
          self.source_port.append(x.value)
        elif x.var_type is VarType.DPORT:
          self.destination_port.append(x.value)
        # do we have a list of protocols?
        elif x.var_type is VarType.PROTOCOL:
          self.protocol.append(x.value)
        # do we have a list of protocol-exceptions?
        elif x.var_type is VarType.PROTOCOL_EXCEPT:
          self.protocol_except.append(x.value)
        # do we have a list of options?
        elif x.var_type is VarType.OPTION:
          self.option.append(x.value)
        elif x.var_type is VarType.SPFX:
          self.source_prefix.append(x.value)
        elif x.var_type is VarType.ESPFX:
          self.source_prefix_except.append(x.value)
        elif x.var_type is VarType.DPFX:
          self.destination_prefix.append(x.value)
        elif x.var_type is VarType.EDPFX:
          self.destination_prefix_except.append(x.value)
        elif x.var_type is VarType.ETHER_TYPE:
          self.ether_type.append(x.value)
        elif x.var_type is VarType.TRAFFIC_TYPE:
          self.traffic_type.append(x.value)
        elif x.var_type is VarType.PRECEDENCE:
          self.precedence.append(x.value)
        elif x.var_type is VarType.FORWARDING_CLASS:
          self.forwarding_class.append(x.value)
        elif x.var_type is VarType.FORWARDING_CLASS_EXCEPT:
          self.forwarding_class_except.append(x.value)
        elif x.var_type is VarType.PAN_APPLICATION:
          self.pan_application.append(x.value)
        elif x.var_type is VarType.NEXT_IP:
          self.next_ip = DEFINITIONS.GetNetAddr(x.value)
        elif x.var_type is VarType.PLATFORM:
          self.platform.append(x.value)
        elif x.var_type is VarType.PLATFORMEXCLUDE:
          self.platform_exclude.append(x.value)
        elif x.var_type is VarType.DSCP_MATCH:
          self.dscp_match.append(x.value)
        elif x.var_type is VarType.DSCP_EXCEPT:
          self.dscp_except.append(x.value)
        elif x.var_type is VarType.STAG:
          self.source_tag.append(x.value)
        elif x.var_type is VarType.DTAG:
          self.destination_tag.append(x.value)
        elif x.var_type is VarType.FLEXIBLE_MATCH_RANGE:
          self.flexible_match_range.append(x.value)
        elif x.var_type is VarType.TARGET_RESOURCES:
          self.target_resources.append(x.value)
        elif x.var_type is VarType.TARGET_SERVICE_ACCOUNTS:
          self.target_service_accounts.append(x.value)
        else:
          raise TermObjectTypeError(
              '%s isn\'t a type I know how to deal with (contains \'%s\')' % (
                  type(x), x.value))
    else:
      # stupid no switch statement in python
      if obj.var_type is VarType.RESTRICT_ADDRESS_FAMILY:
        self.restrict_address_family = obj.value
      elif obj.var_type is VarType.COMMENT:
        self.comment.append(str(obj))
      elif obj.var_type is VarType.OWNER:
        self.owner = obj.value
      elif obj.var_type is VarType.EXPIRATION:
        self.expiration = obj.value
      elif obj.var_type is VarType.LOSS_PRIORITY:
        self.loss_priority = obj.value
      elif obj.var_type is VarType.ROUTING_INSTANCE:
        self.routing_instance = obj.value
      elif obj.var_type is VarType.PRECEDENCE:
        self.precedence = obj.value
      elif obj.var_type is VarType.FORWARDING_CLASS:
        self.forwarding_class.append(obj.value)
      elif obj.var_type is VarType.FORWARDING_CLASS_EXCEPT:
        self.forwarding_class_except.append(obj.value)
      elif obj.var_type is VarType.PAN_APPLICATION:
        self.pan_application.append(obj.value)
      elif obj.var_type is VarType.NEXT_IP:
        self.next_ip = DEFINITIONS.GetNetAddr(obj.value)
      elif obj.var_type is VarType.VERBATIM:
        self.verbatim.append(obj.value)
      elif obj.var_type is VarType.ACTION:
        if str(obj) not in ACTIONS:
          raise InvalidTermActionError('%s is not a valid action' % obj)
        self.action.append(obj.value)
      elif obj.var_type is VarType.COUNTER:
        self.counter = obj
      elif obj.var_type is VarType.ENCAPSULATE:
        self.encapsulate = obj.value
      elif obj.var_type is VarType.TRAFFIC_CLASS_COUNT:
        self.traffic_class_count = obj
      elif obj.var_type is VarType.ICMP_TYPE:
        self.icmp_type.extend(obj.value)
      elif obj.var_type is VarType.ICMP_CODE:
        self.icmp_code.extend(obj.value)
      elif obj.var_type is VarType.LOGGING:
        if str(obj) not in _LOGGING:
          raise InvalidTermLoggingError('%s is not a valid logging option' %
                                        obj)
        self.logging.append(obj)
      elif obj.var_type is VarType.LOG_LIMIT:
        self.log_limit = obj.value
      elif obj.var_type is VarType.LOG_NAME:
        self.log_name = obj.value
      # police man, tryin'a take you jail
      elif obj.var_type is VarType.POLICER:
        self.policer = obj.value
      elif obj.var_type is VarType.PRIORITY:
        self.priority = obj.value
      # qos?
      elif obj.var_type is VarType.QOS:
        self.qos = obj.value
      elif obj.var_type is VarType.PACKET_LEN:
        self.packet_length = obj.value
      elif obj.var_type is VarType.FRAGMENT_OFFSET:
        self.fragment_offset = obj.value
      elif obj.var_type is VarType.HOP_LIMIT:
        self.hop_limit = obj.value
      elif obj.var_type is VarType.SINTERFACE:
        self.source_interface = obj.value
      elif obj.var_type is VarType.DINTERFACE:
        self.destination_interface = obj.value
      elif obj.var_type is VarType.TIMEOUT:
        self.timeout = obj.value
      elif obj.var_type is VarType.DSCP_SET:
        self.dscp_set = obj.value
      elif obj.var_type is VarType.VPN:
        self.vpn = (obj.value[0], obj.value[1])
      elif obj.var_type is VarType.TTL:
        self.ttl = int(obj.value)
      elif obj.var_type is VarType.TARGET_RESOURCES:
        self.target_resources.append(obj.value)
      elif obj.var_type is VarType.TARGET_SERVICE_ACCOUNTS:
        self.target_service_accounts.append(obj.value)
      elif obj.var_type is VarType.FILTER_TERM:
        self.filter_term = obj.value
      else:
        raise TermObjectTypeError(
            '%s isn\'t a type I know how to deal with' % (type(obj)))

  def SanityCheck(self):
    """Sanity check the definition of the term.

    Raises:
      ParseError: if term has both verbatim and non-verbatim tokens
      TermInvalidIcmpType: if term has invalid icmp-types specified
      TermNoActionError: if the term doesn't have an action defined.
      TermPortProtocolError: if the term has a service/protocol definition pair
        which don't match up, eg. SNMP and tcp
      TermAddressExclusionError: if one of the *-exclude directives is defined,
        but that address isn't contained in the non *-exclude directive. eg:
        source-address::CORP_INTERNAL source-exclude:: LOCALHOST
      TermProtocolEtherTypeError: if the term has both ether-type and
        upper-layer protocol restrictions
      InvalidTermActionError: action and routing-instance both defined
      InvalidTermTTLValue: TTL value is invalid.
      MixedPortandNonPortProtos: Ports specified with protocol that doesn't
        support ports.

    This should be called when the term is fully formed, and
    all of the options are set.

    """
    if self.verbatim:
      if (self.action or self.source_port or self.destination_port or
          self.port or self.protocol or self.option):
        raise ParseError(
            'term "%s" has both verbatim and non-verbatim tokens.' % self.name)
    else:
      if (not self.action and not self.routing_instance and not self.next_ip and
          not self.encapsulate and not self.filter_term):
        raise TermNoActionError('no action specified for term %s' % self.name)
      if self.filter_term and self.action:
        raise InvalidTermActionError('term "%s" has both filter and action tokens.' % self.name)
      # have we specified a port with a protocol that doesn't support ports?
      protos_no_ports = {p for p in self.protocol if p not in PROTOS_WITH_PORTS}
      if protos_no_ports != set() and (self.source_port or
                                       self.destination_port or self.port):
        if set(self.protocol) - protos_no_ports != set():
          # This is a more specific error - some protocols support, but not all
          raise MixedPortandNonPortProtos(
              'Term %s contains mixed uses of protocols with and without port '
              'numbers\nProtocols: %s' % (self.name, self.protocol))
        else:
          raise TermPortProtocolError(
              'ports specified with protocol(s) that don\'t support ports. '
              'Term: %s Protocols: %s ' % (self.name, protos_no_ports))

    # TODO(pmoody): do we have mutually exclusive options?
    # eg. tcp-established + tcp-initial?

    if self.ether_type and (
        self.protocol or
        self.address or
        self.destination_address or
        self.destination_address_exclude or
        self.destination_port or
        self.destination_prefix or
        self.destination_prefix_except or
        self.source_address or
        self.source_address_exclude or
        self.source_port or
        self.source_prefix or
        self.source_prefix_except):
      raise TermProtocolEtherTypeError(
          'ether-type not supported when used with upper-layer protocol '
          'restrictions. Term: %s' % self.name)
    # validate icmp-types if specified, but addr_family will have to be checked
    # in the generators as policy module doesn't know about that at this point.
    if self.icmp_code:
      if len(self.icmp_type) != 1:
        raise ICMPCodeError('ICMP Code used with invalid number of types.'
                            'Use only one ICMP type.\n Term: %s' % self.name)
      type_name = self.icmp_type[0]
      bad_codes = []
      for code in self.icmp_code:
        if code not in self.ICMP_CODE[type_name]:
          bad_codes.append(code)
      if bad_codes:
        raise ICMPCodeError('ICMP Codes %s are invalid for ICMP Type %s.'
                            '\nTerm: %s' % (bad_codes, type_name, self.name))
    if self.icmp_type:
      for icmptype in self.icmp_type:
        if (icmptype not in self.ICMP_TYPE[4] and icmptype not in
            self.ICMP_TYPE[6]):
          raise TermInvalidIcmpType('Term %s contains an invalid icmp-type:'
                                    '%s' % (self.name, icmptype))

    if self.ttl:
      if not _MIN_TTL <= self.ttl <= _MAX_TTL:

        raise InvalidTermTTLValue('Term %s contains invalid TTL: %s'
                                  % (self.name, self.ttl))

  def AddressCleanup(self, optimize=True, addressbook=False):
    """Do Address and Port collapsing.

    Notes:
      Collapses both the address definitions and the port definitions
      to their smallest possible length.

    Args:
      optimize: boolean value indicating whether to optimize addresses
      addressbook: Boolean indicating if addressbook is used.
    """
    def cleanup(addresses, complement_addresses):
      if not optimize:
        return nacaddr.SortAddrList(addresses)
      if addressbook:
        return nacaddr.CollapseAddrListPreserveTokens(addresses)
      else:
        return nacaddr.CollapseAddrList(addresses, complement_addresses)

    # address collapsing.
    if self.address:
      self.address = cleanup(self.address, None)

    if self.source_address:
      self.source_address = cleanup(self.source_address,
                                    self.source_address_exclude)
    if self.source_address_exclude:
      self.source_address_exclude = cleanup(self.source_address_exclude,
                                            self.source_address)

    if self.destination_address:
      self.destination_address = cleanup(self.destination_address,
                                         self.destination_address_exclude)
    if self.destination_address_exclude:
      self.destination_address_exclude = cleanup(
          self.destination_address_exclude, self.destination_address)

    # port collapsing.
    if self.port:
      self.port = self.CollapsePortList(self.port)
    if self.source_port:
      self.source_port = self.CollapsePortList(self.source_port)
    if self.destination_port:
      self.destination_port = self.CollapsePortList(self.destination_port)

  def CollapsePortList(self, ports):
    """Given a list of ports, Collapse to the smallest required.

    Args:
      ports: a list of port strings eg: [(80,80), (53,53) (2000, 2009),
                                         (1024,65535)]

    Returns:
      ret_array: the collapsed sorted list of ports, eg: [(53,53), (80,80),
                                                          (1024,65535)]
    """
    ret_ports = []
    for port in sorted(ports):
      if not ret_ports:
        ret_ports.append(port)
      elif ret_ports[-1][1] >= port[1]:
        # (10, 20) and (12, 13) -> (10, 20)
        pass
      elif port[0] < ret_ports[-1][1] < port[1]:
        # (10, 20) and (15, 30) -> (10, 30)
        ret_ports[-1] = (ret_ports[-1][0], port[1])
      elif ret_ports[-1][1] + 1 == port[0]:
        # (10, 20) and (21, 30) -> (10, 30)
        ret_ports[-1] = (ret_ports[-1][0], port[1])
      else:
        # (10, 20) and (22, 30) -> (10, 20), (22, 30)
        ret_ports.append(port)
    return ret_ports

  def CheckProtocolIsContained(self, superset, subset):
    """Check if the given list of protocols is wholly contained.

    Args:
      superset: list of protocols
      subset: list of protocols

    Returns:
      bool: True if subset is contained in superset. false otherwise.
    """
    if not superset:
      return True
    if not subset:
      return False

    # Convert these lists to sets to use set comparison.
    sup = set(superset)
    sub = set(subset)
    return sub.issubset(sup)

  def CheckPortIsContained(self, superset, subset):
    """Check if the given list of ports is wholly contained.

    Args:
      superset: list of port tuples
      subset: list of port tuples

    Returns:
      bool: True if subset is contained in superset, false otherwise
    """
    if not superset:
      return True
    if not subset:
      return False

    for sub_port in subset:
      not_contains = True
      for sup_port in superset:
        if (int(sub_port[0]) >= int(sup_port[0])
            and int(sub_port[1]) <= int(sup_port[1])):
          not_contains = False
          break
      if not_contains:
        return False
    return True

  def CheckAddressIsContained(self, superset, subset):
    """Check if subset is wholey contained by superset.

    Args:
      superset: list of the superset addresses
      subset: list of the subset addresses

    Returns:
      True or False.
    """
    if not superset:
      return True
    if not subset:
      return False

    for sub_addr in subset:
      sub_contained = False
      for sup_addr in superset:
        # ipaddr ensures that version numbers match for inclusion.
        if sub_addr.subnet_of(sup_addr):
          sub_contained = True
          break
      if not sub_contained:
        return False
    return True


class VarType:
  """Generic object meant to store lots of basic policy types."""

  COMMENT = 0
  COUNTER = 1
  ACTION = 2
  SADDRESS = 3
  DADDRESS = 4
  ADDRESS = 5
  SPORT = 6
  DPORT = 7
  PROTOCOL_EXCEPT = 8
  OPTION = 9
  PROTOCOL = 10
  SADDREXCLUDE = 11
  DADDREXCLUDE = 12
  LOGGING = 13
  QOS = 14
  POLICER = 15
  PACKET_LEN = 16
  FRAGMENT_OFFSET = 17
  ICMP_TYPE = 18
  SPFX = 19
  DPFX = 20
  ETHER_TYPE = 21
  TRAFFIC_TYPE = 22
  VERBATIM = 23
  LOSS_PRIORITY = 24
  ROUTING_INSTANCE = 25
  PRECEDENCE = 26
  SINTERFACE = 27
  EXPIRATION = 28
  DINTERFACE = 29
  PLATFORM = 30
  PLATFORMEXCLUDE = 31
  PORT = 32
  TIMEOUT = 33
  OWNER = 34
  ADDREXCLUDE = 36
  VPN = 37
  APPLY_GROUPS = 38
  APPLY_GROUPS_EXCEPT = 39
  DSCP_SET = 40
  DSCP_MATCH = 41
  DSCP_EXCEPT = 42
  FORWARDING_CLASS = 43
  STAG = 44
  DTAG = 45
  NEXT_IP = 46
  HOP_LIMIT = 47
  LOG_NAME = 48
  FLEXIBLE_MATCH_RANGE = 49
  ESPFX = 50
  EDPFX = 51
  FORWARDING_CLASS_EXCEPT = 52
  TRAFFIC_CLASS_COUNT = 53
  PAN_APPLICATION = 54
  ICMP_CODE = 55
  PRIORITY = 56
  TTL = 57
  LOG_LIMIT = 58
  TARGET_RESOURCES = 59
  TARGET_SERVICE_ACCOUNTS = 60
  ENCAPSULATE = 61
  FILTER_TERM = 62
  RESTRICT_ADDRESS_FAMILY = 63


  def __init__(self, var_type, value):
    self.var_type = var_type
    if self.var_type == self.COMMENT or self.var_type == self.LOG_NAME:
      # remove the double quotes
      val = str(value).strip('"')
      # make all of the lines start w/o leading whitespace.
      self.value = '\n'.join([x.lstrip() for x in val.splitlines()])
    else:
      self.value = value

  def __str__(self):
    return str(self.value)

  def __repr__(self):
    return self.__str__()

  def __eq__(self, other):
    return self.var_type == other.var_type and self.value == other.value

  def __hash__(self):
    return id(self)


class Header:
  """The header of the policy file contains the targets and a global comment."""

  def __init__(self):
    self.target = []
    self.comment = []
    self.apply_groups = []
    self.apply_groups_except = []

  def AddObject(self, obj):
    """Add and object to the Header.

    Args:
      obj: of type VarType.COMMENT, VarType.APPLY_GROUPS,
      VarType.APPLY_GROUPS_EXCEPT, or Target

    Raises:
      RuntimeError: if object type cannot be determined
    """
    if type(obj) == Target:
      self.target.append(obj)
    elif isinstance(obj, list) and all(isinstance(x, VarType) for x in obj):
      for x in obj:
        if x.var_type == VarType.APPLY_GROUPS:
          self.apply_groups.append(str(x))
        elif x.var_type == VarType.APPLY_GROUPS_EXCEPT:
          self.apply_groups_except.append(str(x))
    elif obj.var_type == VarType.COMMENT:
      self.comment.append(str(obj))
    else:
      raise RuntimeError('Unable to add object from header.')

  @property
  def platforms(self):
    """The platform targets of this particular header."""
    return [x.platform for x in self.target]

  def FilterOptions(self, platform):
    """Given a platform return the options.

    Args:
      platform: string

    Returns:
      list or None
    """
    for target in self.target:
      if target.platform == platform:
        return target.options
    return []

  def FilterName(self, platform):
    """Given a filter_type, return the filter name.

    Args:
      platform: string

    Returns:
      filter_name: string or None

    Notes:
      !! Deprecated in favor of Header.FilterOptions(platform) !!
    """
    for target in self.target:
      if target.platform == platform:
        if target.options:
          return target.options[0]
    return None

  def __str__(self):
    return 'Target[%s], Comments [%s], Apply groups: [%s], except: [%s]' % (
        ', '.join(map(str, self.target)),
        ', '.join(self.comment),
        ', '.join(self.apply_groups),
        ', '.join(self.apply_groups_except))

  def __repr__(self):
    return self.__str__()

  def __eq__(self, obj):
    """Compares for equality against another Header object.

    Note that it is picky and requires the list contents to be in the
    same order.

    Args:
      obj: object to be compared to for equality.
    Returns:
      True if all the list member variables of this object are equal to the list
      member variables of obj and False otherwise.
    """
    if not isinstance(obj, Header):
      return False
    if self.target != obj.target:
      return False
    if self.comment != obj.comment:
      return False
    if self.apply_groups != obj.apply_groups:
      return False
    if self.apply_groups_except != obj.apply_groups_except:
      return False
    return True


# This could be a VarType object, but I'm keeping it as it's class
# b/c we're almost certainly going to have to do something more exotic with
# it shortly to account for various rendering options like default iptables
# policies or output file names, etc. etc.
class Target:
  """The type of acl to be rendered from this policy file."""

  def __init__(self, target):
    self.platform = target[0]
    self.options = target[1:]

  def __str__(self):
    return self.platform

  def __repr__(self):
    return self.__str__()

  def __eq__(self, other):
    return self.platform == other.platform and self.options == other.options

  def __ne__(self, other):
    return not self.__eq__(other)


# Lexing/Parsing starts here
tokens = (
    'ACTION',
    'ADDR',
    'ADDREXCLUDE',
    'RESTRICT_ADDRESS_FAMILY',
    'COMMENT',
    'COUNTER',
    'DADDR',
    'DADDREXCLUDE',
    'DINTERFACE',
    'DPFX',
    'EDPFX',
    'DPORT',
    'DQUOTEDSTRING',
    'DSCP',
    'DSCP_EXCEPT',
    'DSCP_MATCH',
    'DSCP_RANGE',
    'DSCP_SET',
    'DTAG',
    'ENCAPSULATE',
    'ESCAPEDSTRING',
    'ETHER_TYPE',
    'EXPIRATION',
    'FILTER_TERM',
    'FLEXIBLE_MATCH_RANGE',
    'FORWARDING_CLASS',
    'FORWARDING_CLASS_EXCEPT',
    'FRAGMENT_OFFSET',
    'HOP_LIMIT',
    'APPLY_GROUPS',
    'APPLY_GROUPS_EXCEPT',
    'HEADER',
    'HEX',
    'ICMP_TYPE',
    'ICMP_CODE',
    'INTEGER',
    'LOGGING',
    'LOG_LIMIT',
    'LOG_NAME',
    'LOSS_PRIORITY',
    'LPAREN',
    'LSQUARE',
    'NEXT_IP',
    'OPTION',
    'OWNER',
    'PACKET_LEN',
    'PLATFORM',
    'PLATFORMEXCLUDE',
    'POLICER',
    'PORT',
    'PRECEDENCE',
    'PRIORITY',
    'PROTOCOL',
    'PROTOCOL_EXCEPT',
    'QOS',
    'RPAREN',
    'RSQUARE',
    'PAN_APPLICATION',
    'ROUTING_INSTANCE',
    'SADDR',
    'SADDREXCLUDE',
    'SINTERFACE',
    'SPFX',
    'ESPFX',
    'SPORT',
    'STAG',
    'STRING',
    'TARGET',
    'TARGET_RESOURCES',
    'TARGET_SERVICE_ACCOUNTS',
    'TERM',
    'TIMEOUT',
    'TRAFFIC_CLASS_COUNT',
    'TRAFFIC_TYPE',
    'TTL',
    'VERBATIM',
    'VPN',
)

literals = r':{},-/'
t_ignore = ' \t'
t_LSQUARE = r'\['
t_RSQUARE = r'\]'
t_LPAREN = r'\('
t_RPAREN = r'\)'

reserved = {
    'action': 'ACTION',
    'address': 'ADDR',
    'address-exclude': 'ADDREXCLUDE',
    'restrict-address-family': 'RESTRICT_ADDRESS_FAMILY',
    'comment': 'COMMENT',
    'counter': 'COUNTER',
    'destination-address': 'DADDR',
    'destination-exclude': 'DADDREXCLUDE',
    'destination-interface': 'DINTERFACE',
    'destination-prefix': 'DPFX',
    'destination-prefix-except': 'EDPFX',
    'destination-port': 'DPORT',
    'destination-tag': 'DTAG',
    'dscp-except': 'DSCP_EXCEPT',
    'dscp-match': 'DSCP_MATCH',
    'dscp-set': 'DSCP_SET',
    'encapsulate': 'ENCAPSULATE',
    'ether-type': 'ETHER_TYPE',
    'expiration': 'EXPIRATION',
    'filter-term': 'FILTER_TERM',
    'flexible-match-range': 'FLEXIBLE_MATCH_RANGE',
    'forwarding-class': 'FORWARDING_CLASS',
    'forwarding-class-except': 'FORWARDING_CLASS_EXCEPT',
    'fragment-offset': 'FRAGMENT_OFFSET',
    'hex': 'HEX',
    'hop-limit': 'HOP_LIMIT',
    'apply-groups': 'APPLY_GROUPS',
    'apply-groups-except': 'APPLY_GROUPS_EXCEPT',
    'header': 'HEADER',
    'icmp-type': 'ICMP_TYPE',
    'icmp-code': 'ICMP_CODE',
    'logging': 'LOGGING',
    'log-limit': 'LOG_LIMIT',
    'log_name': 'LOG_NAME',
    'loss-priority': 'LOSS_PRIORITY',
    'next-ip': 'NEXT_IP',
    'option': 'OPTION',
    'owner': 'OWNER',
    'packet-length': 'PACKET_LEN',
    'platform': 'PLATFORM',
    'platform-exclude': 'PLATFORMEXCLUDE',
    'policer': 'POLICER',
    'port': 'PORT',
    'precedence': 'PRECEDENCE',
    'priority': 'PRIORITY',
    'protocol': 'PROTOCOL',
    'protocol-except': 'PROTOCOL_EXCEPT',
    'qos': 'QOS',
    'pan-application': 'PAN_APPLICATION',
    'routing-instance': 'ROUTING_INSTANCE',
    'source-address': 'SADDR',
    'source-exclude': 'SADDREXCLUDE',
    'source-interface': 'SINTERFACE',
    'source-prefix': 'SPFX',
    'source-prefix-except': 'ESPFX',
    'source-port': 'SPORT',
    'source-tag': 'STAG',
    'target': 'TARGET',
    'target-resources': 'TARGET_RESOURCES',
    'target-service-accounts': 'TARGET_SERVICE_ACCOUNTS',
    'term': 'TERM',
    'timeout': 'TIMEOUT',
    'traffic-class-count': 'TRAFFIC_CLASS_COUNT',
    'traffic-type': 'TRAFFIC_TYPE',
    'ttl': 'TTL',
    'verbatim': 'VERBATIM',
    'vpn': 'VPN',
}

# disable linting warnings for lexx/yacc code
# pylint: disable=unused-argument,invalid-name,g-short-docstring-punctuation
# pylint: disable=g-docstring-quotes,g-short-docstring-space
# pylint: disable=g-space-before-docstring-summary,g-doc-args
# pylint: disable=g-no-space-after-docstring-summary
# pylint: disable=g-docstring-missing-newline


def t_IGNORE_COMMENT(t):
  r'\#.*'
  pass


def t_ESCAPEDSTRING(t):
  r'"([^"\\]*(?:\\"[^"\\]*)+)"'
  t.lexer.lineno += str(t.value).count('\n')
  return t


def t_DQUOTEDSTRING(t):
  r'"[^"]*?"'
  t.lexer.lineno += str(t.value).count('\n')
  return t


def t_newline(t):
  r'\n+'
  t.lexer.lineno += len(t.value)


def t_error(t):
  print("Illegal character '%s' on line %s" % (t.value[0], t.lineno))
  t.lexer.skip(1)


def t_DSCP_RANGE(t):
  # pylint: disable=line-too-long
  r'\b((b[0-1]{6})|(af[1-4]{1}[1-3]{1})|(be)|(ef)|(cs[0-7]{1}))([-]{1})((b[0-1]{6})|(af[1-4]{1}[1-3]{1})|(be)|(ef)|(cs[0-7]{1}))\b'
  t.type = reserved.get(t.value, 'DSCP_RANGE')
  return t


def t_DSCP(t):
  # we need to handle the '-' as part of the word, not as a boundary
  r'\b((b[0-1]{6})|(af[1-4]{1}[1-3]{1})|(be)|(ef)|(cs[0-7]{1}))(?![\w-])\b'
  t.type = reserved.get(t.value, 'DSCP')
  return t


def t_HEX(t):
  r'0x[a-fA-F0-9]+'
  return t


def t_INTEGER(t):
  r'\d+'
  return t


def t_STRING(t):
  r'\w+([-_+.@/]\w*)*'
  # we have an identifier; let's check if it's a keyword or just a string.
  t.type = reserved.get(t.value, 'STRING')
  return t


###
## parser starts here
###
def p_target(p):
  """ target : target header terms
             | """
  if len(p) > 1:
    if type(p[1]) is Policy:
      p[1].AddFilter(p[2], p[3])
      p[0] = p[1]
    else:
      p[0] = Policy(p[2], p[3])


def p_header(p):
  """ header : HEADER '{' header_spec '}' """
  p[0] = p[3]


def p_header_spec(p):
  """ header_spec : header_spec target_spec
                  | header_spec comment_spec
                  | header_spec apply_groups_spec
                  | header_spec apply_groups_except_spec
                  | """
  if len(p) > 1:
    if type(p[1]) == Header:
      p[1].AddObject(p[2])
      p[0] = p[1]
    else:
      p[0] = Header()
      p[0].AddObject(p[2])


# we may want to change this at some point if we want to be clever with things
# like being able to set a default input/output policy for iptables policies.
def p_target_spec(p):
  """ target_spec : TARGET ':' ':' strings_or_ints """
  p[0] = Target(p[4])


def p_terms(p):
  """ terms : terms TERM STRING '{' term_spec '}'
            | """
  if len(p) > 1:
    p[5].name = p[3]
    if type(p[1]) == list:
      p[1].append(p[5])
      p[0] = p[1]
    else:
      p[0] = [p[5]]


def p_term_spec(p):
  """ term_spec : term_spec action_spec
                | term_spec addr_spec
                | term_spec restrict_address_family_spec
                | term_spec comment_spec
                | term_spec counter_spec
                | term_spec traffic_class_count_spec
                | term_spec dscp_set_spec
                | term_spec dscp_match_spec
                | term_spec dscp_except_spec
                | term_spec encapsulate_spec
                | term_spec ether_type_spec
                | term_spec exclude_spec
                | term_spec expiration_spec
                | term_spec filter_term_spec
                | term_spec flexible_match_range_spec
                | term_spec forwarding_class_spec
                | term_spec forwarding_class_except_spec
                | term_spec fragment_offset_spec
                | term_spec hop_limit_spec
                | term_spec icmp_type_spec
                | term_spec icmp_code_spec
                | term_spec interface_spec
                | term_spec logging_spec
                | term_spec log_limit_spec
                | term_spec log_name_spec
                | term_spec losspriority_spec
                | term_spec next_ip_spec
                | term_spec option_spec
                | term_spec owner_spec
                | term_spec packet_length_spec
                | term_spec platform_spec
                | term_spec policer_spec
                | term_spec port_spec
                | term_spec precedence_spec
                | term_spec priority_spec
                | term_spec prefix_list_spec
                | term_spec protocol_spec
                | term_spec qos_spec
                | term_spec pan_application_spec
                | term_spec routinginstance_spec
                | term_spec tag_list_spec
                | term_spec target_resources_spec
                | term_spec target_service_accounts_spec
                | term_spec timeout_spec
                | term_spec ttl_spec
                | term_spec traffic_type_spec
                | term_spec verbatim_spec
                | term_spec vpn_spec
                | """
  if len(p) > 1:
    if type(p[1]) == Term:
      p[1].AddObject(p[2])
      p[0] = p[1]
    else:
      p[0] = Term(p[2])

def p_restrict_address_family_spec(p):
  """ restrict_address_family_spec : RESTRICT_ADDRESS_FAMILY ':' ':' STRING """
  p[0] = VarType(VarType.RESTRICT_ADDRESS_FAMILY, p[4])

def p_routinginstance_spec(p):
  """ routinginstance_spec : ROUTING_INSTANCE ':' ':' STRING """
  p[0] = VarType(VarType.ROUTING_INSTANCE, p[4])


def p_losspriority_spec(p):
  """ losspriority_spec :  LOSS_PRIORITY ':' ':' STRING """
  p[0] = VarType(VarType.LOSS_PRIORITY, p[4])


def p_precedence_spec(p):
  """ precedence_spec : PRECEDENCE ':' ':' one_or_more_ints """
  p[0] = VarType(VarType.PRECEDENCE, p[4])


def p_flexible_match_range_spec(p):
  """ flexible_match_range_spec : FLEXIBLE_MATCH_RANGE ':' ':' flex_match_key_values """
  p[0] = []
  for kv in p[4]:
    p[0].append(VarType(VarType.FLEXIBLE_MATCH_RANGE, kv))


def p_flex_match_key_values(p):
  """ flex_match_key_values : flex_match_key_values STRING HEX
                            | flex_match_key_values STRING INTEGER
                            | flex_match_key_values STRING STRING
                            | STRING HEX
                            | STRING INTEGER
                            | STRING STRING
                            | """
  if len(p) < 1:
    return

  if p[1] not in _FLEXIBLE_MATCH_RANGE_ATTRIBUTES:
    raise FlexibleMatchError('%s is not a valid attribute' % p[1])
  if p[1] == 'match-start':
    if p[2] not in _FLEXIBLE_MATCH_START_OPTIONS:
      raise FlexibleMatchError('%s value is not valid' % p[1])
  # per Juniper, max bit length is 32
  elif p[1] == 'bit-length':
    if int(p[2]) not in list(range(33)):
      raise FlexibleMatchError('%s value is not valid' % p[1])
  # per Juniper, max bit offset is 7
  elif p[1] == 'bit-offset':
    if int(p[2]) not in list(range(8)):
      raise FlexibleMatchError('%s value is not valid' % p[1])
  # per Juniper, offset can be up to 256 bytes
  elif p[1] == 'byte-offset':
    if int(p[2]) not in list(range(256)):
      raise FlexibleMatchError('%s value is not valid' % p[1])

  if type(p[0]) == type([]):
    p[0].append([p.slice[1:]])
  else:
    p[0] = [[i.value for i in p.slice[1:]]]


def p_forwarding_class_spec(p):
  """ forwarding_class_spec : FORWARDING_CLASS ':' ':' one_or_more_strings """
  p[0] = []
  for fclass in p[4]:
    p[0].append(VarType(VarType.FORWARDING_CLASS, fclass))


def p_forwarding_class_except_spec(p):
  """ forwarding_class_except_spec : FORWARDING_CLASS_EXCEPT ':' ':' one_or_more_strings """
  p[0] = []
  for fclass in p[4]:
    p[0].append(VarType(VarType.FORWARDING_CLASS_EXCEPT, fclass))


def p_next_ip_spec(p):
  """ next_ip_spec : NEXT_IP ':' ':' STRING """
  p[0] = VarType(VarType.NEXT_IP, p[4])


def p_encapsulate_spec(p):
  """ encapsulate_spec : ENCAPSULATE ':' ':' STRING """
  p[0] = VarType(VarType.ENCAPSULATE, p[4])


def p_icmp_type_spec(p):
  """ icmp_type_spec : ICMP_TYPE ':' ':' one_or_more_strings """
  p[0] = VarType(VarType.ICMP_TYPE, p[4])

def p_icmp_code_spec(p):
  """ icmp_code_spec : ICMP_CODE ':' ':' one_or_more_ints """
  p[0] = VarType(VarType.ICMP_CODE, p[4])

def p_priority_spec(p):
  """ priority_spec : PRIORITY ':' ':' INTEGER """
  p[0] = VarType(VarType.PRIORITY, p[4])

def p_packet_length_spec(p):
  """ packet_length_spec : PACKET_LEN ':' ':' INTEGER
                         | PACKET_LEN ':' ':' INTEGER '-' INTEGER """
  if len(p) == 5:
    p[0] = VarType(VarType.PACKET_LEN, str(p[4]))
  else:
    p[0] = VarType(VarType.PACKET_LEN, str(p[4]) + '-' + str(p[6]))


def p_fragment_offset_spec(p):
  """ fragment_offset_spec : FRAGMENT_OFFSET ':' ':' INTEGER
                           | FRAGMENT_OFFSET ':' ':' INTEGER '-' INTEGER """
  if len(p) == 5:
    p[0] = VarType(VarType.FRAGMENT_OFFSET, str(p[4]))
  else:
    p[0] = VarType(VarType.FRAGMENT_OFFSET, str(p[4]) + '-' + str(p[6]))


def p_hop_limit_spec(p):
  """ hop_limit_spec : HOP_LIMIT ':' ':' INTEGER
                     | HOP_LIMIT ':' ':' INTEGER '-' INTEGER """
  if len(p) == 5:
    p[0] = VarType(VarType.HOP_LIMIT, str(p[4]))
  else:
    p[0] = VarType(VarType.HOP_LIMIT, str(p[4]) + '-' + str(p[6]))


def p_one_or_more_dscps(p):
  """ one_or_more_dscps : one_or_more_dscps DSCP_RANGE
                        | one_or_more_dscps DSCP
                        | one_or_more_dscps INTEGER
                        | DSCP_RANGE
                        | DSCP
                        | INTEGER """
  if len(p) > 1:
    if type(p[1]) is list:
      p[1].append(p[2])
      p[0] = p[1]
    else:
      p[0] = [p[1]]


def p_dscp_set_spec(p):
  """ dscp_set_spec : DSCP_SET ':' ':' DSCP
                    | DSCP_SET ':' ':' INTEGER """
  p[0] = VarType(VarType.DSCP_SET, p[4])


def p_dscp_match_spec(p):
  """ dscp_match_spec : DSCP_MATCH ':' ':' one_or_more_dscps """
  p[0] = []
  for dscp in p[4]:
    p[0].append(VarType(VarType.DSCP_MATCH, dscp))


def p_dscp_except_spec(p):
  """ dscp_except_spec : DSCP_EXCEPT ':' ':' one_or_more_dscps """
  p[0] = []
  for dscp in p[4]:
    p[0].append(VarType(VarType.DSCP_EXCEPT, dscp))


def p_exclude_spec(p):
  """ exclude_spec : SADDREXCLUDE ':' ':' one_or_more_strings
                   | DADDREXCLUDE ':' ':' one_or_more_strings
                   | ADDREXCLUDE ':' ':' one_or_more_strings
                   | PROTOCOL_EXCEPT ':' ':' one_or_more_strings """

  p[0] = []
  for ex in p[4]:
    if p[1].find('source-exclude') >= 0:
      p[0].append(VarType(VarType.SADDREXCLUDE, ex))
    elif p[1].find('destination-exclude') >= 0:
      p[0].append(VarType(VarType.DADDREXCLUDE, ex))
    elif p[1].find('address-exclude') >= 0:
      p[0].append(VarType(VarType.ADDREXCLUDE, ex))
    elif p[1].find('protocol-except') >= 0:
      p[0].append(VarType(VarType.PROTOCOL_EXCEPT, ex))


def p_prefix_list_spec(p):
  """ prefix_list_spec : DPFX ':' ':' one_or_more_strings
                       | EDPFX ':' ':' one_or_more_strings
                       | SPFX ':' ':' one_or_more_strings
                       | ESPFX ':' ':' one_or_more_strings """
  p[0] = []
  for pfx in p[4]:
    if p[1].find('source-prefix-except') >= 0:
      p[0].append(VarType(VarType.ESPFX, pfx))
    elif p[1].find('source-prefix') >= 0:
      p[0].append(VarType(VarType.SPFX, pfx))
    elif p[1].find('destination-prefix-except') >= 0:
      p[0].append(VarType(VarType.EDPFX, pfx))
    elif p[1].find('destination-prefix') >= 0:
      p[0].append(VarType(VarType.DPFX, pfx))


def p_addr_spec(p):
  """ addr_spec : SADDR ':' ':' one_or_more_strings
                | DADDR ':' ':' one_or_more_strings
                | ADDR  ':' ':' one_or_more_strings """
  p[0] = []
  for addr in p[4]:
    if p[1].find('source-address') >= 0:
      p[0].append(VarType(VarType.SADDRESS, addr))
    elif p[1].find('destination-address') >= 0:
      p[0].append(VarType(VarType.DADDRESS, addr))
    else:
      p[0].append(VarType(VarType.ADDRESS, addr))


def p_port_spec(p):
  """ port_spec : SPORT ':' ':' one_or_more_strings
                | DPORT ':' ':' one_or_more_strings
                | PORT ':' ':' one_or_more_strings """
  p[0] = []
  for port in p[4]:
    if p[1].find('source-port') >= 0:
      p[0].append(VarType(VarType.SPORT, port))
    elif p[1].find('destination-port') >= 0:
      p[0].append(VarType(VarType.DPORT, port))
    else:
      p[0].append(VarType(VarType.PORT, port))


def p_protocol_spec(p):
  """ protocol_spec : PROTOCOL ':' ':' strings_or_ints """
  p[0] = []
  for proto in p[4]:
    p[0].append(VarType(VarType.PROTOCOL, proto))


def p_tag_list_spec(p):
  """ tag_list_spec : DTAG ':' ':' one_or_more_strings
                    | STAG ':' ':' one_or_more_strings """
  p[0] = []
  for tag in p[4]:
    if p[1].find('source-tag') >= 0:
      p[0].append(VarType(VarType.STAG, tag))
    elif p[1].find('destination-tag') >= 0:
      p[0].append(VarType(VarType.DTAG, tag))


def p_target_resources_spec(p):
  """ target_resources_spec : TARGET_RESOURCES ':' ':' one_or_more_tuples
  """
  p[0] = []
  for target_resource in p[4]:
    p[0].append(VarType(VarType.TARGET_RESOURCES, target_resource))


def p_target_service_accounts_spec(p):
  """ target_service_accounts_spec : TARGET_SERVICE_ACCOUNTS ':' ':' one_or_more_strings """
  p[0] = []
  for service_account in p[4]:
    p[0].append(VarType(VarType.TARGET_SERVICE_ACCOUNTS, service_account))


def p_ether_type_spec(p):
  """ ether_type_spec : ETHER_TYPE ':' ':' one_or_more_strings """
  p[0] = []
  for proto in p[4]:
    p[0].append(VarType(VarType.ETHER_TYPE, proto))


def p_traffic_type_spec(p):
  """ traffic_type_spec : TRAFFIC_TYPE ':' ':' one_or_more_strings """
  p[0] = []
  for proto in p[4]:
    p[0].append(VarType(VarType.TRAFFIC_TYPE, proto))


def p_policer_spec(p):
  """ policer_spec : POLICER ':' ':' STRING """
  p[0] = VarType(VarType.POLICER, p[4])


def p_logging_spec(p):
  """ logging_spec : LOGGING ':' ':' STRING """
  p[0] = VarType(VarType.LOGGING, p[4])


def p_log_limit_spec(p):
  """ log_limit_spec : LOG_LIMIT ':' ':' INTEGER '/' STRING"""
  p[0] = VarType(VarType.LOG_LIMIT, (p[4], p[6]))


def p_log_name_spec(p):
  """ log_name_spec : LOG_NAME ':' ':' DQUOTEDSTRING """
  p[0] = VarType(VarType.LOG_NAME, p[4])


def p_option_spec(p):
  """ option_spec : OPTION ':' ':' one_or_more_strings """
  p[0] = []
  for opt in p[4]:
    p[0].append(VarType(VarType.OPTION, opt))


def p_action_spec(p):
  """ action_spec : ACTION ':' ':' STRING """
  p[0] = VarType(VarType.ACTION, p[4])


def p_counter_spec(p):
  """ counter_spec : COUNTER ':' ':' STRING """
  p[0] = VarType(VarType.COUNTER, p[4])


def p_traffic_class_count_spec(p):
  """ traffic_class_count_spec : TRAFFIC_CLASS_COUNT ':' ':' STRING """
  p[0] = VarType(VarType.TRAFFIC_CLASS_COUNT, p[4])


def p_expiration_spec(p):
  """ expiration_spec : EXPIRATION ':' ':' INTEGER '-' INTEGER '-' INTEGER """
  p[0] = VarType(VarType.EXPIRATION, datetime.date(int(p[4]),
                                                   int(p[6]),
                                                   int(p[8])))


def p_comment_spec(p):
  """ comment_spec : COMMENT ':' ':' DQUOTEDSTRING """
  p[0] = VarType(VarType.COMMENT, p[4])


def p_owner_spec(p):
  """ owner_spec : OWNER ':' ':' STRING """
  p[0] = VarType(VarType.OWNER, p[4])


def p_verbatim_spec(p):
  """ verbatim_spec : VERBATIM ':' ':' STRING DQUOTEDSTRING
                    | VERBATIM ':' ':' STRING ESCAPEDSTRING """
  p[0] = VarType(VarType.VERBATIM, [p[4], p[5].strip('"').replace('\\"', '"')])


def p_vpn_spec(p):
  """ vpn_spec : VPN ':' ':' STRING STRING
               | VPN ':' ':' STRING """
  if len(p) == 6:
    p[0] = VarType(VarType.VPN, [p[4], p[5]])
  else:
    p[0] = VarType(VarType.VPN, [p[4], ''])


def p_qos_spec(p):
  """ qos_spec : QOS ':' ':' STRING """
  p[0] = VarType(VarType.QOS, p[4])


def p_pan_application_spec(p):
  """ pan_application_spec : PAN_APPLICATION ':' ':' one_or_more_strings """
  p[0] = []
  for apps in p[4]:
    p[0].append(VarType(VarType.PAN_APPLICATION, apps))


def p_interface_spec(p):
  """ interface_spec : SINTERFACE ':' ':' STRING
                     | DINTERFACE ':' ':' STRING """
  if p[1].find('source-interface') >= 0:
    p[0] = VarType(VarType.SINTERFACE, p[4])
  elif p[1].find('destination-interface') >= 0:
    p[0] = VarType(VarType.DINTERFACE, p[4])


def p_platform_spec(p):
  """ platform_spec : PLATFORM ':' ':' one_or_more_strings
                    | PLATFORMEXCLUDE ':' ':' one_or_more_strings """
  p[0] = []
  for platform in p[4]:
    if p[1].find('platform-exclude') >= 0:
      p[0].append(VarType(VarType.PLATFORMEXCLUDE, platform))
    elif p[1].find('platform') >= 0:
      p[0].append(VarType(VarType.PLATFORM, platform))


def p_apply_groups_spec(p):
  """ apply_groups_spec : APPLY_GROUPS ':' ':' one_or_more_strings """
  p[0] = []
  for group in p[4]:
    p[0].append(VarType(VarType.APPLY_GROUPS, group))


def p_apply_groups_except_spec(p):
  """ apply_groups_except_spec : APPLY_GROUPS_EXCEPT ':' ':' one_or_more_strings
  """
  p[0] = []
  for group_except in p[4]:
    p[0].append(VarType(VarType.APPLY_GROUPS_EXCEPT, group_except))


def p_timeout_spec(p):
  """ timeout_spec : TIMEOUT ':' ':' INTEGER """
  p[0] = VarType(VarType.TIMEOUT, p[4])


def p_ttl_spec(p):
  """ ttl_spec : TTL ':' ':' INTEGER """
  p[0] = VarType(VarType.TTL, p[4])

def p_filter_term_spec(p):
  """ filter_term_spec : FILTER_TERM ':' ':' STRING """
  p[0] = VarType(VarType.FILTER_TERM, p[4])

def p_one_or_more_strings(p):
  """ one_or_more_strings : one_or_more_strings STRING
                          | STRING
                          | """
  if len(p) > 1:
    if type(p[1]) == type([]):
      p[1].append(p[2])
      p[0] = p[1]
    else:
      p[0] = [p[1]]

def p_one_or_more_tuples(p):
  """ one_or_more_tuples : LSQUARE one_or_more_tuples RSQUARE
                         | one_or_more_tuples ',' one_tuple
                         | one_or_more_tuples one_tuple
                         | one_tuple
                         | """

  if len(p) > 1:
    if p[1] == '[':
      p[0] = p[2]
    elif type(p[1]) == type([]):
      if p[2] == ',':
        p[1].append(p[3])
      else:
        p[1].append(p[2])
      p[0] = p[1]
    else:
      p[0] = [p[1]]


def p_one_tuple(p):
  """ one_tuple : LPAREN STRING ',' STRING RPAREN
                | """
  p[0] = (p[2], p[4])


def p_one_or_more_ints(p):
  """ one_or_more_ints : one_or_more_ints INTEGER
                      | INTEGER
                      | """
  if len(p) > 1:
    if type(p[1]) == type([]):
      p[1].append(int(p[2]))
      p[0] = p[1]
    else:
      p[0] = [int(p[1])]


def p_strings_or_ints(p):
  """ strings_or_ints : strings_or_ints STRING
                      | strings_or_ints INTEGER
                      | STRING
                      | INTEGER
                      | """
  if len(p) > 1:
    if type(p[1]) is list:
      p[1].append(p[2])
      p[0] = p[1]
    else:
      p[0] = [p[1]]


def p_error(p):
  """."""
  global parser
  next_token = parser.token()
  if next_token is None:
    use_token = 'EOF'
  else:
    use_token = repr(next_token.value)

  if p:
    raise ParseError(' ERROR on "%s" (type %s, line %d, Next %s)'
                     % (p.value, p.type, p.lineno, use_token))
  else:
    raise ParseError(' ERROR you likely have unablanaced "{"\'s')

parser = yacc.yacc(write_tables=False, debug=0, errorlog=yacc.NullLogger())

# pylint: enable=unused-argument,invalid-name,g-short-docstring-punctuation
# pylint: enable=g-docstring-quotes,g-short-docstring-space
# pylint: enable=g-space-before-docstring-summary,g-doc-args
# pylint: enable=g-no-space-after-docstring-summary
# pylint: enable=g-docstring-missing-newline


def _ReadFile(filename):
  """Read data from a file if it exists.

  Args:
    filename: str - Filename

  Returns:
    data: str contents of file.

  Raises:
    FileNotFoundError: if requested file does not exist.
    FileReadError: Any error resulting from trying to open/read file.
  """
  logging.debug('ReadFile(%s)', filename)
  if os.path.exists(filename):
    try:
      data = open(filename, 'r').read()
      return data
    except IOError:
      raise FileReadError('Unable to open or read file %s' % filename)
  else:
    raise FileNotFoundError('Unable to open policy file %s' % filename)


def _Preprocess(data, max_depth=5, base_dir=''):
  """Search input for include statements and import specified include file.

  Search input for include statements and if found, import specified file
  and recursively search included data for includes as well up to max_depth.

  Args:
    data: A string of Policy file data.
    max_depth: Maximum depth of included files
    base_dir: Base path string where to look for policy or include files

  Returns:
    A string containing result of the processed input data

  Raises:
    RecursionTooDeepError: nested include files exceed maximum
  """
  if not max_depth:
    raise RecursionTooDeepError('%s' % (
        'Included files exceed maximum recursion depth of %s.' % max_depth))
  rval = []
  for line in [x.rstrip() for x in data.splitlines()]:
    words = line.split()
    if len(words) > 1 and words[0] == '#include':
      # remove any quotes around included filename
      include_file = words[1].strip('\'"')
      data = _ReadFile(os.path.join(base_dir, include_file))
      # recursively handle includes in included data
      inc_data = _Preprocess(data, max_depth - 1, base_dir=base_dir)
      rval.extend(inc_data)
    else:
      rval.append(line)
  return rval


def ParseFile(filename, definitions=None, optimize=True, base_dir='',
              shade_check=False):
  """Parse the policy contained in file, optionally provide a naming object.

  Read specified policy file and parse into a policy object.

  Args:
    filename: Name of policy file to parse.
    definitions: optional naming library definitions object.
    optimize: bool - whether to summarize networks and services.
    base_dir: base path string to look for acls or include files.
    shade_check: bool - whether to raise an exception when a term is shaded.

  Returns:
    policy object or False (if parse error).
  """
  data = _ReadFile(filename)
  p = ParsePolicy(data, definitions, optimize, base_dir=base_dir,
                  shade_check=shade_check, filename=filename)
  return p


def ParsePolicy(data, definitions=None, optimize=True, base_dir='',
                shade_check=False, filename=''):
  """Parse the policy in 'data', optionally provide a naming object.

  Parse a blob of policy text into a policy object.

  Args:
    data: a string blob of policy data to parse.
    definitions: optional naming library definitions object.
    optimize: bool - whether to summarize networks and services.
    base_dir: base path string to look for acls or include files.
    shade_check: bool - whether to raise an exception when a term is shaded.
    filename: string - filename used by the policy.

  Returns:
    policy object or False (if parse error).
  """
  try:
    if definitions:
      globals()['DEFINITIONS'] = definitions
    else:
      globals()['DEFINITIONS'] = naming.Naming(DEFAULT_DEFINITIONS)
    globals()['_OPTIMIZE'] = optimize
    globals()['_SHADE_CHECK'] = shade_check

    lexer = lex.lex()

    preprocessed_data = '\n'.join(_Preprocess(data, base_dir=base_dir))
    global parser
    policy = parser.parse(preprocessed_data, lexer=lexer)
    policy.filename = filename
    return policy

  except IndexError:
    return False


# if you call this from the command line, you can specify a pol file for it to
# read.
if __name__ == '__main__':
  ret = 0
  if len(sys.argv) > 1:
    try:
      ret = ParsePolicy(open(sys.argv[1], 'r').read(), filename=sys.argv[1])
    except IOError:
      print('ERROR: \'%s\' either does not exist or is not readable' %
            (sys.argv[1]))
      ret = 1
  else:
    # default to reading stdin
    ret = ParsePolicy(sys.stdin.read())
  sys.exit(ret)


# capirca/lib/policy_simple.py

# Copyright 2015 Google 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.

"""A simple, line-oriented parser for Caprica policies.

This parser differs from the default parser in that it preserves the original
structure and defers resolving referents to the user. This is useful for
analyzing policy structures and their use of naming data. It happens to discard
inline comments but preservers line-level comments. Fields expected to have
"naming" values are stored as a set without order or line breaks retained.
"""

from absl import logging
import six


class Field:
  """A name-value assignment within a block."""

  def __init__(self, value):
    self.value = value

  def __str__(self):
    t = type(self)
    f = 'UNKNOWN'
    for k, v in field_map.items():
      if t == v:
        f = k
        break
    indent = len(f) + 5
    return '%s::%s' % (f, self.ValueStr().replace('\n', '\n' + ' ' * indent))

  def __eq__(self, o):
    if not isinstance(o, self.__class__):
      return False
    return self.value == o.value

  def __ne__(self, o):
    return not self == o

  def Append(self, value):
    self.value += value

  def ValueStr(self):
    return self.value


class IntegerField(Field):

  def __init__(self, value):
    super().__init__(value)
    try:
      _ = int(value)
    except ValueError:
      raise ValueError('Invalid integer field: "%s"' % str(self))


class NamingField(Field):
  """A naming field is one that refers to names in used in naming.py."""

  def __init__(self, value):
    super().__init__(value)
    self.value = self.ParseString(value)

  def ParseString(self, value):
    """Split and validate a string value into individual names."""
    parts = set(value.split())
    for p in parts:
      self.ValidatePart(p)
    return parts

  def ValidatePart(self, part):
    """Validate that a string smells like a naming.py name."""
    for c in part:
      if c not in '-_.' and not c.isdigit() and not c.isupper():
        raise ValueError('Invalid name reference: "%s"' % part)

  def Append(self, value):
    """Split, validate, and add name contained within a string."""
    parts = self.ParseString(value)
    self.value.update(parts)

  def ValueStr(self):
    """Return the value as a series of lines no longer than 60 chars each."""
    values = sorted(self.value)
    line_wrap = 60
    length = 0
    line_buf = []
    value_buf = []
    for v in values:
      if length + len(v) > line_wrap:
        value_buf.append(' '.join(line_buf))
        length = 0
        line_buf = []
      else:
        if line_buf:
          length += 1
        line_buf.append(v)
        length += len(v)
    if line_buf:
      value_buf.append(' '.join(line_buf))
    return ' ' + '\n'.join(value_buf)


class Action(Field):
  """An action field."""


class Address(NamingField):
  """An address field."""


class Port(NamingField):
  """A port field."""


class Comment(Field):
  """A comment field."""

  def ValueStr(self):
    # Comments should align with the string contents, after the leading
    # quotation mark.
    return self.value.replace('\n', '\n ')


class Counter(Field):
  """A counter field."""


class Encapsulate(Field):
  """An encapsulate field."""


class DestinationAddress(Address):
  """A destination-address field."""


class DestinationExclude(Address):
  """A destination-exclude field."""


class DestinationInterface(Field):
  """A destination-interface field."""


class DestinationPort(Port):
  """A destination-port field."""


class DestinationPrefix(Field):
  """A destination-prefix field."""


class DestinationPrefixExcept(Field):
  """A destination-prefix-except field."""


class DestinationTag(Field):
  """A destination tag field."""


class DscpMatch(Field):
  """A dscp-match field."""


class DscpSet(Field):
  """A dscp-set field."""


class EtherType(Field):
  """An ether-type field."""


class Expiration(Field):
  """An expiration field."""


class FragmentOffset(Field):
  """A fragment-offset field."""


class ForwardingClass(Field):
  """A forwarding-class field."""


class ForwardingClassExcept(Field):
  """A forwarding-class-except field."""


class IcmpCode(Field):
  """A icmp-code field."""


class IcmpType(Field):
  """A icmp-type field."""


class Logging(Field):
  """A logging field."""


class LossPriority(Field):
  """A loss-priority field."""


class Option(Field):
  """An Option field."""


class Owner(Field):
  """An owner field."""


class NextIP(Field):
  """An owner field."""


class PacketLength(Field):
  """A packet-length field."""


class Platform(Field):
  """A platform field."""


class PlatformExclude(Field):
  """A platform-exclude field."""


class Policer(Field):
  """A rate-limit-icmp field."""


class Precedence(Field):
  """A precedence field."""


class Protocol(Field):
  """A Protocol field."""


class ProtocolExcept(Field):
  """A protocol-except field."""


class Qos(Field):
  """A rate-limit-icmp field."""


class PANApplication(Field):
  """A rate-limit-icmp field."""


class RoutingInstance(Field):
  """A routing-instance field."""


class SourceAddress(Address):
  """A source-address field."""


class SourceExclude(Address):
  """A source-exclude field."""


class SourceInterface(Field):
  """A source-interface field."""


class SourcePort(Port):
  """A source-port field."""


class SourcePrefix(Field):
  """A source-prefix field."""


class SourcePrefixExcept(Field):
  """A source-prefix-except field."""


class SourceTag(Field):
  """A source tag field."""


class Target(Field):
  """A target field."""


class Timeout(IntegerField):
  """A timeout field."""


class TrafficType(Field):
  """A traffic-type field."""


class TrafficClassCount(Field):
  """A traffic-class-count field."""


class Verbatim(Field):
  """A verbatim field."""


class Vpn(Field):
  """A vpn field."""


destination_address_fields = (DestinationAddress, DestinationExclude,
                              DestinationPrefix)

field_map = {
    'action': Action,
    'address': Address,
    'comment': Comment,
    'counter': Counter,
    'destination-address': DestinationAddress,
    'destination-exclude': DestinationExclude,
    'destination-interface': DestinationInterface,
    'destination-port': DestinationPort,
    'destination-prefix': DestinationPrefix,
    'destination-prefix-except': DestinationPrefixExcept,
    'destination-tag': DestinationTag,
    'dscp-match': DscpMatch,
    'dscp-set': DscpSet,
    'ether-type': EtherType,
    'expiration': Expiration,
    'fragment-offset': FragmentOffset,
    'forwarding-class': ForwardingClass,
    'forwarding-class-except': ForwardingClassExcept,
    'icmp-code': IcmpCode,
    'icmp-type': IcmpType,
    'logging': Logging,
    'loss-priority': LossPriority,
    'option': Option,
    'owner': Owner,
    'next-ip': NextIP,
    'packet-length': PacketLength,
    'platform': Platform,
    'platform-exclude': PlatformExclude,
    'policer': Policer,
    'port': Port,
    'precedence': Precedence,
    'protocol': Protocol,
    'protocol-except': ProtocolExcept,
    'qos': Qos,
    'pan-application': PANApplication,
    'routing-instance': RoutingInstance,
    'source-address': SourceAddress,
    'source-exclude': SourceExclude,
    'source-interface': SourceInterface,
    'source-port': SourcePort,
    'source-prefix': SourcePrefix,
    'source-prefix-except': SourcePrefixExcept,
    'source-tag': SourceTag,
    'target': Target,
    'timeout': Timeout,
    'traffic-class-count': TrafficClassCount,
    'traffic-type': TrafficType,
    'verbatim': Verbatim,
    'vpn': Vpn,
    'encapsulate': Encapsulate,
}


class Block:
  """A section containing fields."""

  def __init__(self):
    self.fields = []

  def __iter__(self):
    return iter(self.fields)

  def __getitem__(self, i):
    return self.fields[i]

  def __str__(self):
    buf = []
    buf.append(type(self).__name__.lower())
    buf.append(' ')
    if self.Name():
      buf.append(self.Name())
      buf.append(' ')
    buf.append('{')  # }
    buf.append('\n')
    for field in self.fields:
      buf.append('  ')
      buf.append(str(field))
      buf.append('\n')
    buf.append('}')
    buf.append('\n')
    return ''.join(buf)

  def AddField(self, field):
    if not issubclass(type(field), Field):
      raise TypeError('%s not subclass of Field.' % field)
    self.fields.append(field)

  def FieldsWithType(self, f_type):
    if not issubclass(f_type, Field):
      raise TypeError('%s not subclass of Field.' % f_type)
    return [x for x in self.fields if isinstance(x, f_type)]

  def Match(self, match_fn):
    """Yield the fields and their indices for which match_fn is True."""
    for i, f in enumerate(self.fields):
      if match_fn(f):
        yield i, f

  def Name(self):
    return ''

  def __eq__(self, o):
    if not isinstance(o, self.__class__):
      return False
    if len(self.fields) != len(o.fields):
      return False
    for mine, theirs in zip(self.fields, o.fields):
      logging.debug('testing "%s" vs "%s"', mine, theirs)
      if mine != theirs:
        return False
    return True

  def __ne__(self, o):
    return not self == o


class Header(Block):
  """A header block."""


class Term(Block):
  """A policy term."""

  def __init__(self, name):
    super().__init__()
    self.name = name

  def Name(self):
    return self.name

  def __eq__(self, o):
    if not super().__eq__(o):
      return False
    return self.name == o.name

  def Describe(self):
    """Return a human-readable description of the term."""
    verbatims = self.FieldsWithType(Verbatim)
    if verbatims:
      return 'Verbatim: %s' % verbatims

    handled = set()
    handled.update(self.FieldsWithType(Comment))

    pieces = []
    actions = self.FieldsWithType(Action)
    if len(actions) != 1:
      raise ValueError('No action or multiple actions.')
    handled.update(actions)
    pieces.append(actions[0].value.title() + ' traffic')

    protocols = self.FieldsWithType(Protocol)
    all_protocols = set()
    if protocols:
      handled.update(protocols)
      for protocol in protocols:
        all_protocols.update(protocol.value.split())
      pieces.append('using ' + ' or '.join(sorted(all_protocols)))

    icmp_code = self.FieldsWithType(IcmpCode)
    all_icmp_code = set()
    if icmp_code:
      handled.update(icmp_code)
      for code in icmp_code:
        all_icmp_code.update(code.value.split())
      pieces.append('(ICMP code %s)' % ', '.join(sorted(all_icmp_code)))

    icmp_types = self.FieldsWithType(IcmpType)
    all_icmp_types = set()
    if icmp_types:
      handled.update(icmp_types)
      for icmp_type in icmp_types:
        all_icmp_types.update(icmp_type.value.split())
      pieces.append('(ICMP types %s)' % ', '.join(sorted(all_icmp_types)))

    sources = self.FieldsWithType(SourceAddress)
    if sources:
      handled.update(sources)
      pieces.append('originating from')
      all_sources = set()
      for source in sources:
        all_sources.update(source.value)
      pieces.append(', '.join(sorted(all_sources)))

    source_ports = self.FieldsWithType(SourcePort)
    if source_ports:
      handled.update(source_ports)
      if sources:
        pieces.append('using port')
      else:
        pieces.append('originating port')
      all_sources = set()
      for source in source_ports:
        all_sources.update(source.value)
      pieces.append(', '.join(sorted(all_sources)))

    destinations = self.FieldsWithType(DestinationAddress)
    if destinations:
      handled.update(destinations)
      pieces.append('destined for')
      all_destinations = set()
      for destination in destinations:
        all_destinations.update(destination.value)
      pieces.append(', '.join(sorted(all_destinations)))

    destination_ports = self.FieldsWithType(DestinationPort)
    if destination_ports:
      handled.update(destination_ports)
      if destinations:
        pieces.append('on port')
      else:
        pieces.append('destined for port')
      all_destinations = set()
      for destination in destination_ports:
        all_destinations.update(destination.value)
      pieces.append(', '.join(sorted(all_destinations)))

    vpns = self.FieldsWithType(Vpn)
    if vpns:
      handled.update(vpns)
      pieces.append('via VPNs')
      pieces.append(','.join(x.value for x in vpns))

    # Ignore some fields
    for ignored_type in (Expiration, Owner):
      ignored_fields = self.FieldsWithType(ignored_type)
      if ignored_fields:
        handled.update(ignored_fields)

    for field in self:
      if field not in handled:
        raise ValueError('Uncovered field: ' + str(field))
    return ' '.join(pieces)


class BlankLine:
  """A blank line."""

  def __str__(self):
    return '\n'

  def __eq__(self, o):
    return isinstance(o, self.__class__)

  def __ne__(self, o):
    return not self == o


class CommentLine:
  """A comment in the file."""

  def __init__(self, data):
    self.data = data

  def __str__(self):
    return str(self.data) + '\n'

  def __eq__(self, o):
    if not isinstance(o, self.__class__):
      return False
    return self.data == o.data

  def __ne__(self, o):
    return not self == o


class Include:
  """A reference to another policy definition."""

  def __init__(self, identifier):
    self.identifier = identifier

  def __str__(self):
    return '#include %s' % self.identifier

  def __eq__(self, o):
    if not isinstance(o, self.__class__):
      return False
    return self.identifier == o.identifier

  def __ne__(self, o):
    return not self == o


class Policy:
  """An ordered list of headers, terms, comments, blank lines and includes."""

  def __init__(self, identifier):
    self.identifier = identifier
    self.members = []

  def AddMember(self, member):
    m_type = type(member)
    if (m_type not in (Include, CommentLine, BlankLine)
        and not issubclass(m_type, Block)):
      raise TypeError('%s must be a Block, CommentLine, BlankLine,'
                      ' or Include' % m_type)
    self.members.append(member)

  def __str__(self):
    return ''.join(str(x) for x in self.members)

  def __iter__(self):
    return iter(self.members)

  def __getitem__(self, i):
    return self.members[i]

  def Match(self, match_fn):
    """Yield the members and their indices for which match_fn is True."""
    for i, m in enumerate(self.members):
      if match_fn(m):
        yield i, m

  def MatchFields(self, block_match_fn, field_match_fn):
    for match_idx, m in self.Match(block_match_fn):
      if not isinstance(m, Block):
        continue
      for field_idx, f in m.Match(field_match_fn):
        yield match_idx, field_idx, f


class PolicyParser:
  """Parse a policy object from a data buffer."""

  def __init__(self, data, identifier):
    self.data = data
    self.identifier = identifier
    self.block_in_progress = None
    self.policy = None

  def Parse(self):
    """Do the needful."""
    self.policy = Policy(self.identifier)
    for line in self.data.split('\n'):
      line = line.strip()
      logging.debug('Processing line: "%s"', line)
      if self.block_in_progress:
        self.ParseInBlock(line)
      else:
        self.ParseTopLevel(line)
    if self.block_in_progress:
      raise ValueError('Unexpected EOF reading "%s"' % self.block_in_progress)
    return self.policy

  def ParseTopLevel(self, line):
    """Parse a line not nested within a block."""
    if line == '':  # pylint: disable=g-explicit-bool-comparison
      self.policy.AddMember(BlankLine())
      return
    if line.startswith('#'):
      if line.startswith('#include '):
        self.ParseIncludeLine(line)
        return
      self.ParseCommentLine(line)
      return
    if line.startswith('header {') or line.startswith('header{'):  # }
      self.ParseHeaderLine(line)
      return
    if line.startswith('term '):
      self.ParseTermLine(line)
      return
    raise ValueError('Unhandled top-level line %s' % line)

  def ParseCommentLine(self, line):
    """Parse a line with a line level comment."""
    if self.block_in_progress:
      raise ValueError('Found comment line in block: %s' % line)
    self.policy.AddMember(CommentLine(line))

  def ParseIncludeLine(self, line):
    """Parse an #include line refering to another file."""
    if self.block_in_progress:
      raise ValueError('Found include line in block: %s' % line)
    line_parts = line.split()
    if len(line_parts) < 2:
      raise ValueError('Invalid include: %s' % line)
    inc_ref = line_parts[1]
    if '#' in inc_ref:
      inc_ref, _ = inc_ref.split('#', 1)
    self.policy.AddMember(Include(inc_ref))

  def ParseHeaderLine(self, line):
    """Parse a line beginning a header block."""
    if self.block_in_progress:
      raise ValueError('Nested blocks not allowed: %s' % line)
    self.block_in_progress = Header()

  def ParseTermLine(self, line):
    """Parse a line beginning a term block."""
    if self.block_in_progress:
      raise ValueError('Nested blocks not allowed: %s' % line)
    line_parts = line.split()

    # Some terms don't have a space after the name
    if '{' in line_parts[1]:  # }
      brace_idx = line_parts[1].index('{')  # }
      line_parts[1] = line_parts[1][:brace_idx]
    else:
      if not line_parts[2].startswith('{'):  # }
        raise ValueError('Invalid term line: %s' % line)
    term_name = line_parts[1]
    self.block_in_progress = Term(term_name)

  def ParseInBlock(self, line):
    """Parse a line when inside a block definition."""
    if line == '' or line.startswith('#'):  # pylint: disable=g-explicit-bool-comparison
      return
    if '::' in line:
      self.ParseField(line)
      return
    if line.startswith('}'):
      self.policy.AddMember(self.block_in_progress)
      self.block_in_progress = None
      return
    if self.block_in_progress is not None:
      self.block_in_progress.fields[-1].Append('\n' + line)

  def ParseField(self, line):
    """Parse a line containing a block field."""
    name, value = line.split('::', 1)
    name = name.strip().lower()
    f_type = field_map.get(name)
    if not f_type:
      raise ValueError('Invalid field line: %s' % line)
    self.block_in_progress.AddField(f_type(value))


# capirca/lib/policyreader.py

# Copyright 2011 Google 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.
#

"""Utility to provide exploration of policy definition files.

Allows read only access of policy definition files.  The library
creates a Policy object, which has filters containing terms.

This library does no expansion on the tokens directly, such as in policy.py.

TODO: This library is currently incomplete, and does not allow access to
      every argument of a policy term.
"""

from capirca.lib import naming


class Error(Exception):
  """Generic error class."""


class FileOpenError(Error):
  """Trouble opening a file."""


class InvalidFilterError(Error):
  """Filter is invalid."""


class Filter:
  """Simple filter with a name a list of terms."""

  def __init__(self, filtername=''):
    self.name = filtername
    self.term = []

  def __str__(self):
    rval = []
    title = 'Filter: %s' % str(self.name)
    rval.append('\n%s' % title)
    rval.append('-' * len(title))
    for term in self.term:
      rval.append(str(term))
    return '\n\n'.join(rval)


class Term:
  """Simple term with a name a list of attributes."""

  def __init__(self, termname=''):
    self.name = termname
    self.source = []
    self.destination = []
    self.sport = []
    self.dport = []
    self.action = []
    self.option = []
    self.protocol = []

  def __str__(self):
    rval = []
    rval.append('  Term: %s' % self.name)
    rval.append('  Source-address:: %s' % ' '.join(self.source))
    rval.append('  Destination-address:: %s' % ' '.join(self.destination))
    rval.append('  Source-port:: %s' % ' '.join(self.sport))
    rval.append('  Destination-port:: %s' % ' '.join(self.dport))
    rval.append('  Protocol:: %s' % ' '.join(self.protocol))
    rval.append('  Option:: %s' % ' '.join(self.option))
    rval.append('  Action:: %s' % ' '.join(self.action))
    return '\n'.join(rval)


class Policy:
  """Holds basic attributes of an unexpanded policy definition file."""

  def __init__(self, filename, defs_data=None):
    """Build policy object and naming definitions from provided filenames.

    Args:
      filename: location of a .pol file
      defs_data: location of naming definitions directory, if any
    """
    self.defs = naming.Naming(defs_data)
    self.filter = []
    try:
      self.data = open(filename, 'r').readlines()
    except IOError as error_info:
      info = str(filename) + ' cannot be opened'
      raise FileOpenError('%s\n%s' % (info, error_info))

    indent = 0
    in_header = False
    in_term = False
    filt = Filter()
    term = Term()
    in_string = False

    for line in self.data:
      words = line.strip().split()
      quotes = len(line.split('"')) + 1
      if quotes % 2:               # are we in or out of double quotes
        in_string = not in_string  # flip status of quote status
      if not in_string:
        if '{' in words:
          indent += 1
        if words:
          if words[0] == 'header':
            in_header = True
          if words[0] == 'term':
            in_term = True
            term = Term(words[1])
          if in_header and words[0] == 'target::':
            if filt.name != words[2]:  # avoid empty dupe filters due to
              filt = Filter(words[2])  # multiple target header lines
          if in_term:
            if words[0] == 'source-address::':
              term.source.extend(words[1:])
            if words[0] == 'destination-address::':
              term.destination.extend(words[1:])
            if words[0] == 'source-port::':
              term.sport.extend(words[1:])
            if words[0] == 'destination-port::':
              term.dport.extend(words[1:])
            if words[0] == 'action::':
              term.action.extend(words[1:])
            if words[0] == 'protocol::':
              term.protocol.extend(words[1:])
            if words[0] == 'option::':
              term.option.extend(words[1:])

        if '}' in words:
          indent -= 1
          if in_header:
            self.filter.append(filt)
            in_header = False
          if in_term:
            filt.term.append(term)
            in_term = False

  def __str__(self):
    return '\n'.join(str(i) for i in self.filter)

  def Matches(self, src=None, dst=None, dport=None, sport=None,
              filtername=None):
    """Return list of term names that match specific attributes.

    Args:
      src: source ip address '12.1.1.1'
      dst: destination ip address '10.1.1.1'
      dport: any port/protocol combo, such as '80/tcp' or '53/udp'
      sport: any port/protocol combo, such as '80/tcp' or '53/udp'
      filtername: a filter name or None to search all filters

    Returns:
      results: list of lists, each list is index to filter & term in the policy

    Raises:
      InvalidFilterError: Error if filter is invalid.

    Example:
      p=policyreader.Policy('policy_path', 'definitions_path')

      p.Matches(dst='209.85.216.5', dport='25/tcp')
      [[0, 26]]
      print p.filter[0].term[26].name

      for match in p.Matches(dst='209.85.216.5'):
        print p.filter[match[0]].term[match[1]].name

    """
    rval = []
    results = []
    filter_list = []
    dport_parents = None
    sport_parents = None
    destination_parents = None
    source_parents = None
    if dport:
      dport_parents = self.defs.GetServiceParents(dport)
    if sport:
      sport_parents = self.defs.GetServiceParents(sport)
    if dst:
      destination_parents = self.defs.GetIpParents(dst)
      try:
        destination_parents.remove('ANY')
        destination_parents.remove('RESERVED')
      except ValueError:
        pass  # ignore and continue
    if src:
      source_parents = self.defs.GetIpParents(src)
      try:
        source_parents.remove('ANY')
        source_parents.remove('RESERVED')
      except ValueError:
        pass  # ignore and continue
    if not filtername:
      filter_list = self.filter
    else:
      for idx, fil in enumerate(self.filter):
        if filtername == fil.name:
          filter_list = [self.filter[idx]]
      if not filter_list:
        raise InvalidFilterError('invalid filter name: %s' % filtername)

    for findex, xfilter in enumerate(filter_list):
      mterms = []
      mterms.append(set())  # dport
      mterms.append(set())  # sport
      mterms.append(set())  # dst
      mterms.append(set())  # src
      for tindex, term in enumerate(xfilter.term):
        if dport_parents:
          for token in dport_parents:
            if token in term.dport:
              mterms[0].add(tindex)
        else:
          mterms[0].add(tindex)
        if sport_parents:
          for token in sport_parents:
            if token in term.sport:
              mterms[1].add(tindex)
        else:
          mterms[1].add(tindex)
        if destination_parents:
          for token in destination_parents:
            if token in term.destination:
              mterms[2].add(tindex)
        else:
          mterms[2].add(tindex)
        if source_parents:
          for token in source_parents:
            if token in term.source:
              mterms[3].add(tindex)
        else:
          mterms[3].add(tindex)
      rval.append(list(mterms[0] & mterms[1] & mterms[2] & mterms[3]))
    for findex, fresult in enumerate(rval):
      for i in list(fresult):
        results.append([findex, i])
    return results


# capirca/lib/port.py

# Copyright 2011 Google 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.
#

"""Common library for network ports and protocol handling."""



class Error(Exception):
  """Base error class."""


class BadPortValue(Error):
  """Invalid port format."""


class BadPortRange(Error):
  """Out of bounds port range."""


class InvalidRange(Error):
  """Range is not valid (eg, single port)."""


class NotSinglePort(Error):
  """Port range defined instead of a single port."""


class PPP:
  """PPP: [P]ort [P]rotocol [P]airs.

  Make port/protocol pairs an object for easy comparisons
  """

  def __init__(self, service):
    """Init for PPP object.

    Args:
      service: A port/protocol pair as str (eg: '80/tcp', '22-23/tcp') or
               a nested service name (eg: 'SSH')
    """
    # remove comments (if any)
    self.service = service.split('#')[0].strip()
    if '/' in self.service:
      self.port = self.service.split('/')[0]
      self.protocol = self.service.split('/')[1]
      self.nested = False
    else:
      # for nested services
      self.nested = True
      self.port = None
      self.protocol = None

  @property
  def is_range(self):
    if self.port:
      return '-' in self.port
    else:
      return False

  @property
  def is_single_port(self):
    if self.port:
      return '-' not in self.port
    else:
      return False

  @property
  def start(self):
    # return the first port in the range as int
    if '-' in self.port:
      self._start = int(self.port.split('-')[0])
    else:
      raise InvalidRange('%s is not a valid port range' % self.port)
    return self._start

  @property
  def end(self):
    # return the last port in the range as int
    if '-' in self.port:
      self._end = int(self.port.split('-')[1])
    else:
      raise InvalidRange('%s is not a valid port range' % self.port)
    return self._end

  def __contains__(self, other):
    # determine if a single-port object is within another objects' range
    try:
      return ((int(self.start) <= int(other.port) <= int(self.end)) and
              self.protocol == other.protocol)
    except:
      raise InvalidRange('%s must be a range' % self.port)

  def __lt__(self, other):
    if self.is_single_port:
      try:
        return int(self.port) < int(other.port)
      except:
        return False
    else:
      raise NotSinglePort('Comparisons cannot be performed on port ranges')

  def __gt__(self, other):
    if self.is_single_port:
      try:
        return int(self.port) > int(other.port)
      except:
        return False
    else:
      raise NotSinglePort('Comparisons cannot be performed on port ranges')

  def __le__(self, other):
    if self.is_single_port:
      try:
        return int(self.port) <= int(other.port)
      except:
        return False
    else:
      raise NotSinglePort('Comparisons cannot be performed on port ranges')

  def __ge__(self, other):
    if self.is_single_port:
      try:
        return int(self.port) >= int(other.port)
      except:
        return False
    else:
      raise NotSinglePort('Comparisons cannot be performed on port ranges')

  def __eq__(self, other):
    if self.is_single_port:
      try:
        return (int(self.port) == int(other.port) and
                self.protocol == other.protocol)
      except:
        return False
    else:
      raise NotSinglePort('Comparisons cannot be performed on port ranges')


def Port(port):
  """Sanitize a port value.

  Args:
    port: a port value

  Returns:
    port: a port value

  Raises:
    BadPortValue: port is not valid integer or string
    BadPortRange: port is outside valid range
  """
  pval = -1
  try:
    pval = int(port)
  except ValueError:
    raise BadPortValue('port %s is not valid.' % port)
  if pval < 0 or pval > 65535:
    raise BadPortRange('port %s is out of range 0-65535.' % port)
  return pval


# capirca/lib/speedway.py

# Copyright 2011 Google 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.
#


"""Speedway iptables generator.  This is a subclass of Iptables lib."""

from string import Template
from capirca.lib import iptables


class Error(Exception):
  pass


class Term(iptables.Term):
  """Generate Iptables policy terms."""
  _PLATFORM = 'speedway'
  _PREJUMP_FORMAT = None
  _POSTJUMP_FORMAT = Template('-A $filter -j $term')


class Speedway(iptables.Iptables):
  """Generates filters and terms from provided policy object."""

  _PLATFORM = 'speedway'
  _DEFAULT_PROTOCOL = 'all'
  SUFFIX = '.ipt'

  _RENDER_PREFIX = '*filter'
  _RENDER_SUFFIX = 'COMMIT'
  _DEFAULTACTION_FORMAT = ':%s %s'

  _TERM = Term


# capirca/lib/srxlo.py

# Copyright 2014 Google 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.
#

"""Juniper SRX generator for loopback ACLs.

This is a subclass of Juniper generator. Juniper SRX loopback filter
uses the same syntax as regular Juniper stateless ACLs, with minor
differences. This subclass effects those differences.
"""

from capirca.lib import juniper


class Term(juniper.Term):
  """Single SRXlo term representation."""

  _PLATFORM = 'srxlo'

  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.term.protocol = ['icmp6' if x == 'icmpv6' else x
                          for x in self.term.protocol]
    self.term.protocol_except = [
        'icmp6' if x == 'icmpv6' else x for x in self.term.protocol_except
    ]

  def NormalizeIcmpTypes(self, icmp_types, protocols, af):
    protocols = ['icmpv6' if x == 'icmp6' else x for x in protocols]
    return super().NormalizeIcmpTypes(icmp_types, protocols, af)


class SRXlo(juniper.Juniper):
  """SRXlo generator."""
  _PLATFORM = 'srxlo'
  SUFFIX = '.jsl'
  _TERM = Term

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()
    # flexible match is MX/Trio only
    supported_tokens.remove('flexible_match_range')
    # currently only support 'encapsulate' in juniper
    supported_tokens.remove('encapsulate')
    return supported_tokens, supported_sub_tokens


# capirca/lib/summarizer.py

# Copyright 2014 Google 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.
#

"""Discontinuous subnet mask summarizer."""

import collections

from capirca.lib import nacaddr


class DSMNet:
  """Hold IP address information for the purposes of DSM summarization.

    ipaddr maintainers explicitly declared that they will not
    support discontinuous subnet masks, hence this is required.
  """

  def __init__(self, address, netmask, text=''):
    """Creates DSMNet.

    Args:
      address: network address as int.
      netmask: subnet mask as int.
      text: text comment.
    """
    self.address = address
    self.netmask = netmask
    self.text = text

  def __eq__(self, other):
    try:
      return (self.address == other.address and
              self.netmask == other.netmask)
    except AttributeError:
      return NotImplemented

  def __ne__(self, other):
    eq = self.__eq__(other)
    if eq is NotImplemented:
      return NotImplemented
    return not eq

  def __le__(self, other):
    gt = self.__gt__(other)
    if gt is NotImplemented:
      return NotImplemented
    return not gt

  def __ge__(self, other):
    lt = self.__lt__(other)
    if lt is NotImplemented:
      return NotImplemented
    return not lt

  def __lt__(self, other):
    try:
      if self.address != other.address:
        return self.address < other.address
    except AttributeError:
      return NotImplemented
    return False

  def __gt__(self, other):
    try:
      if self.address != other.address:
        return self.address > other.address
    except AttributeError:
      return NotImplemented
    return False

  def __str__(self):
    return ' '.join([self.address, self.netmask])

  def MergeText(self, text=''):
    """Returns self.text joined with optional text.

    Don't join the text if it's already contained in self.text.

    Args:
      text: text to be combined with self.text.

    Returns:
      Combined text.
    """
    if self.text:
      if text and text not in self.text:
        return ', '.join([self.text, text])
      return self.text
    else:
      return text


def ToDottedQuad(net, negate=False, nondsm=False):
  """Turns a DSMNet object into decimal dotted quad tuple.

  Args:
    net: DSMNet object.
    negate: if subnet mask should be negated (and become wildcard).
    nondsm: if mask should be generated in prefixlen when non-DSM.

  Returns:
    tuple (decimal dotted address, decimal dotted mask).

  Raises:
    ValueError: if address is larger than 32 bits or mask is not exactly
      0 or 32 bits.
  """
  if net.address.bit_length() > 32:
    raise ValueError('Addresses larger than 32 bits '
                     'are currently not supported.')
  if net.netmask.bit_length() not in (0, 32):
    raise ValueError('Subnet masks other than 0 or 32 '
                     'are currently not supported.')
  if negate:
    netmask = ~net.netmask
  else:
    netmask = net.netmask

  return (_Int32ToDottedQuad(net.address),
          _PrefixlenForNonDSM(netmask)) if nondsm else (
              _Int32ToDottedQuad(net.address), _Int32ToDottedQuad(netmask))


def _PrefixlenForNonDSM(intmask):
  """Turns 32 bit integer into dotted decimal with JunOS friendly.

  Args:
    intmask: 32 bit integer.

  Returns:
    A string in dotted decimal or prefixlen format.
  """
  dotmask = _Int32ToDottedQuad(intmask)

  if dotmask == '255.255.255.255':
    return '32'

  bitmask = '{:032b}'.format(intmask)

  prefixlen = 0
  while bitmask[prefixlen] == '1':
    prefixlen += 1

  return dotmask if int(bitmask[prefixlen:], 2) else str(prefixlen)


def _Int32ToDottedQuad(num):
  """Turns 32 bit integer into dotted decimal notation.

  Args:
    num: 32 bit integer.

  Returns:
    Integer as a string in dotted decimal notation.

  """
  octets = []
  for _ in range(4):
    octet = num & 0xFF
    octets.insert(0, str(octet))
    num >>= 8
  return '.'.join(octets)


def _NacaddrNetToDSMNet(net):
  """Converts nacaddr.IPv4 or nacaddr.IPv6 object into DSMNet object.

  Args:
    net: nacaddr.IPv4 or nacaddr.IPv6 object.

  Returns:
    DSMNet object.
  """

  # left shift number of subnet mask bits, then leftshift until
  # full length of address reached
  address_as_int = int(net.network_address)
  netmask_as_int = (((1 << net.prefixlen) - 1) <<
                    (net.max_prefixlen - net.prefixlen))
  return DSMNet(address_as_int, netmask_as_int, net.text)


def _ToPrettyBinaryFormat(num):
  """Prettily formatted string of binary representation of suplied number.

  Useful for debugging.

  Args:
    num: number to be prettily formatted

  Returns:
    prettily formatted string
  """
  # like ipaddr make assumption that this is ipv4
  byte_strings = []
  while num > 0 or len(byte_strings) < 4:
    byte_strings.append('{0:08b}'.format(num & 0xff))
    num >>= 8
  return ' '.join(reversed(byte_strings))


def Summarize(nets):
  """Summarizes networks while allowing for discontinuous subnet mask.

  Args:
    nets: list of nacaddr.IPv4 or nacaddr.IPv6 objects.
        Address family can be mixed, however there is no support for rendering
        anything other than IPv4.

  Returns:
    sorted list of DSMIPNet objects.
  """

  result = []
  optimized_nets = nacaddr.CollapseAddrList(nets)
  nets_by_netmask = collections.defaultdict(list)
  # group nets by subnet mask
  for net in optimized_nets:
    nets_by_netmask[net.prefixlen].append(_NacaddrNetToDSMNet(net))
  for nets in nets_by_netmask.values():
    result.extend(_SummarizeSameMask(nets))
  return sorted(result)


def _SummarizeSameMask(nets):
  """Summarizes networks while allowing for discontinuous subnet mask.

  Args:
    nets: list of unique, summarized DSMNet objects with the same netmask.

  Returns:
    sorted list of DSMNet objects that are discontinuously summarized.
  """

  # singletons can not possible be paired and are our result
  singletons = []
  # combinetons can potentially be paired
  combinetons = nets

  while combinetons:
    current_nets = combinetons
    combinetons = []
    while current_nets:
      current_net = current_nets.pop(0)
      # look for pair net, but keep index handy
      for pair_net_index, pair_net in enumerate(current_nets):
        xored_address = current_net.address ^ pair_net.address
        # For networks with the same network mask:
        # check if they have exactly one bit difference
        # or they are "a pair".
        if (current_net.netmask == pair_net.netmask and
            (xored_address & (xored_address - 1) == 0) and xored_address > 0):
          # if pair was found, remove both, add paired up network
          # to combinetons for next run and move along
          # otherwise this network can never be paired
          current_nets.pop(pair_net_index)
          new_netmask = current_net.netmask ^ xored_address
          # summarize supplied networks into one using discontinuous
          # subnet mask.
          combinetons.append(DSMNet(min(current_net.address, pair_net.address),
                                    new_netmask,
                                    current_net.MergeText(pair_net.text)))
          break
      else:
        singletons.append(current_net)
  return singletons


# capirca/lib/windows.py

# Copyright 2016 Google 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.
#

"""Generic Windows security policy generator; requires subclassing."""

import datetime
import string

from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import nacaddr


CMD_PREFIX = 'netsh ipsec static add '


class Term(aclgenerator.Term):
  """Generate generic windows policy terms."""

  _PLATFORM = 'windows'

  _COMMENT_FORMAT = string.Template(': $comment')

  # filter rules
  _ACTION_TABLE = {}

  def __init__(self, term, filter_name, filter_action, af='inet'):
    """Setup a new term.

    Args:
      term: A policy.Term object to represent in windows_ipsec.
      filter_name: The name of the filter chan to attach the term to.
      filter_action: The default action of the filter.
      af: Which address family ('inet' or 'inet6') to apply the term to.

    Raises:
      UnsupportedFilterError: Filter is not supported.
    """
    super().__init__(term)
    self.term = term  # term object
    self.filter = filter_name  # actual name of filter
    self.default_action = filter_action
    self.options = []
    self.af = af

    if af == 'inet6':
      self._all_ips = nacaddr.IPv6('::/0')
    else:
      self._all_ips = nacaddr.IPv4('0.0.0.0/0')

    self.term_name = '%s_%s' % (self.filter[:1], self.term.name)

  def __str__(self):
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if self._PLATFORM not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if self._PLATFORM in self.term.platform_exclude:
        return ''

    ret_str = []

    # Don't render icmpv6 protocol terms under inet, or icmp under inet6
    if ((self.af == 'inet6' and 'icmp' in self.term.protocol) or
        (self.af == 'inet' and 'icmpv6' in self.term.protocol)):
      logging.debug(self.NO_AF_LOG_PROTO.substitute(term=self.term.name,
                                                    proto=self.term.protocol,
                                                    af=self.af))
      return ''

    # append comments to output
    ret_str.append(self._COMMENT_FORMAT.substitute(filter=self.filter,
                                                   term=self.term_name,
                                                   comment=self.term.comment))

    # if terms does not specify action, use filter default action
    if not self.term.action:
      self.term.action[0].value = self.default_action

    if self.term.action[0] == 'next':
      return ''

    if len(self.term.action) > 1:
      raise aclgenerator.UnsupportedFilterError('\n%s %s %s %s' % (
          'Multiple actions unsupported by', self._PLATFORM,
          '\nError in term:', self.term.name))

    # protocol
    if self.term.protocol:
      protocols = self.term.protocol
    else:
      protocols = ['any']

    # addresses
    src_addr = self.term.source_address
    if not src_addr:
      src_addr = [self._all_ips]

    dst_addr = self.term.destination_address
    if not dst_addr:
      dst_addr = [self._all_ips]

    if (self.term.source_address_exclude or
        self.term.destination_address_exclude):
      raise aclgenerator.UnsupportedFilterError('\n%s %s %s %s' % (
          'address exclusions unsupported by', self._PLATFORM,
          '\nError in term:', self.term.name))

    # ports = Map the ports in a straight list since multiports aren't supported
    (src_ports, dst_ports) = self._HandlePorts(self.term.source_port,
                                               self.term.destination_port)

    # The windows ipsec driver requires either 'tcp' or 'udp' to be specified
    # if a srcport or dstport is specified.  Fail if src or dst ports are
    # specified and of the protocols are not exactly one or both of 'tcp'
    # or 'udp'.
    if ((not set(protocols).issubset(set(['tcp', 'udp']))) and
        (len(src_ports) > 1 or len(dst_ports) > 1)):
      raise aclgenerator.UnsupportedFilterError('%s %s %s' % (
          '\n', self.term.name,
          'src or dst ports may only be specified with "tcp" and/or "udp".'))

    # icmp-types
    (icmp_types, protocols) = self._HandleIcmpTypes(self.term.icmp_type,
                                                    protocols)

    ret_str = []
    self._HandlePreRule(ret_str)
    self._CartesianProduct(src_addr, dst_addr, protocols, icmp_types, src_ports,
                           dst_ports, ret_str)
    self._HandlePreRule(ret_str)

    return '\n'.join(str(v) for v in ret_str if v)

  def _HandleIcmpTypes(self, icmp_types, protocols):
    """Perform implementation-specific icmp_type and protocol transforms.

    Note that icmp_types or protocols are passed as parameters in case they
    are to be munged prior to this function call, and may not be identical
    to self.term.* parameters.

    Args:
      icmp_types:  a list of icmp types, e.g., self.term.icmp_types
      protocols:  a list of protocols, e.g., self.term.protocols

    Returns:
      A pair of lists of (icmp_types, protocols)
    """
    return None, None

  def _HandlePorts(self, src_ports, dst_ports):
    """Perform implementation-specific port transforms.

    Note that icmp_types or protocols are passed as parameters in case they
    are to be munged prior to this function call, and may not be identical
    to self.term.* parameters.

    Args:
      src_ports:  list of source port range tuples, e.g., self.term.source_port
      dst_ports:  list of destination port range tuples

    Returns:
      A pair of lists of (icmp_types, protocols)
    """
    return None, None

  def _HandlePreRule(self, ret_str):
    """Perform any pre-cartesian product transforms on the ret_str array.

    Args:
      ret_str:  an array of strings that will eventually be joined to form
        the string output for the term.
    """
    pass

  def _CartesianProduct(self, src_addr, dst_addr, protocol, icmp_types,
                        src_ports, dst_ports, ret_str):
    """Perform any the appropriate cartesian product of the input parameters.

    Args:
      src_addr: a type(IP) list of the source addresses
      dst_addr: a type(IP) list of the destination addresses
      protocol: a string list of the protocols
      icmp_types: a numeric list of the icmp_types
      src_ports: a (start, end) list of the source ports
      dst_ports: a (start,end) list of the destination ports
      ret_str:  an array of strings that will eventually be joined to form
        the string output for the term.
    """
    pass

  def _HandlePostRule(self, ret_str):
    """Perform any port-cartesian product transforms on the ret_str array.

    Args:
      ret_str:  an array of strings that will eventually be joined to form
        the string output for the term.
    """
    pass


class WindowsGenerator(aclgenerator.ACLGenerator):
  """Generates filters and terms from provided policy object."""

  _PLATFORM = 'windows'
  _DEFAULT_PROTOCOL = 'all'
  SUFFIX = '.bat'
  _RENDER_PREFIX = None
  _DEFAULT_ACTION = 'block'
  _TERM = Term

  _GOOD_AFS = ['inet', 'inet6']

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens |= {'option'}
    supported_tokens -= {'verbatim'}

    supported_sub_tokens.update({'action': {'accept', 'deny'}})
    del supported_sub_tokens['option']
    return supported_tokens, supported_sub_tokens

  def _TranslatePolicy(self, pol, exp_info):
    """Translate a policy from objects into strings."""
    self.windows_policies = []
    current_date = datetime.datetime.utcnow().date()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)

    default_action = None
    good_default_actions = ['permit', 'block']
    good_options = []

    for header, terms in pol.filters:
      filter_type = None
      if self._PLATFORM not in header.platforms:
        continue

      filter_options = header.FilterOptions(self._PLATFORM)[1:]
      filter_name = header.FilterName(self._PLATFORM)

      # ensure all options after the filter name are expected
      for opt in filter_options:
        if opt not in good_default_actions + self._GOOD_AFS + good_options:
          raise aclgenerator.UnsupportedTargetOptionError('%s %s %s %s' % (
              '\nUnsupported option found in', self._PLATFORM,
              'target definition:', opt))

      # Check for matching af
      for address_family in self._GOOD_AFS:
        if address_family in filter_options:
          # should not specify more than one AF in options
          if filter_type is not None:
            raise aclgenerator.UnsupportedFilterError('%s %s %s %s' % (
                '\nMay only specify one of', self._GOOD_AFS,
                'in filter options:', filter_options))
          filter_type = address_family
      if filter_type is None:
        filter_type = 'inet'

      # does this policy override the default filter actions?
      for next_target in header.target:
        if next_target.platform == self._PLATFORM:
          if len(next_target.options) > 1:
            for arg in next_target.options:
              if arg in good_default_actions:
                default_action = arg
      if default_action and default_action not in good_default_actions:
        raise aclgenerator.UnsupportedTargetOptionError('%s %s %s %s %s' % (
            '\nOnly', ', '.join(good_default_actions),
            'default filter action allowed;', default_action, 'used.'))

      # add the terms
      new_terms = []
      term_names = set()
      for term in terms:
        if term.name in term_names:
          raise aclgenerator.DuplicateTermError(
              'You have a duplicate term: %s' % term.name)
        term_names.add(term.name)

        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info('INFO: Term %s in policy %s expires '
                         'in less than two weeks.', term.name, filter_name)
          if term.expiration <= current_date:
            logging.warning('WARNING: Term %s in policy %s is expired and '
                            'will not be rendered.', term.name, filter_name)
            continue
        if 'established' in term.option or 'tcp-established' in term.option:
          continue
        new_terms.append(self._TERM(term, filter_name, default_action,
                                    filter_type))

      self.windows_policies.append((header, filter_name, filter_type,
                                    default_action, new_terms))

  def __str__(self):
    target = []
    pretty_platform = '%s%s' % (self._PLATFORM[0].upper(), self._PLATFORM[1:])

    if self._RENDER_PREFIX:
      target.append(self._RENDER_PREFIX)

    for header, _, filter_type, default_action, terms in self.windows_policies:
      # Add comments for this filter
      target.append(': %s %s Policy' % (pretty_platform,
                                        header.FilterName(self._PLATFORM)))

      self._HandlePolicyHeader(header, target)

      # reformat long text comments, if needed
      comments = aclgenerator.WrapWords(header.comment, 70)
      if comments and comments[0]:
        for line in comments:
          target.append(': %s' % line)
        target.append(':')
      # add the p4 tags
      target.extend(aclgenerator.AddRepositoryTags(': '))
      target.append(': ' + filter_type)

      if default_action:
        raise aclgenerator.UnsupportedTargetOptionError(
            'Windows generator does not support default actions')

      # add the terms
      for term in terms:
        term_str = str(term)
        if term_str:
          target.append(term_str)
          self._HandleTermFooter(header, term, target)

    target.append('')
    return '\n'.join(target)

  def _HandlePolicyHeader(self, header, target):
    pass

  def _HandleTermFooter(self, header, term, target):
    pass


# capirca/lib/windows_advfirewall.py

# Copyright 2016 Google 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.
#
"""Windows advfirewall policy generator."""

import string

from capirca.lib import windows


class Term(windows.Term):
  """Generate windows advfirewall policy terms."""

  _PLATFORM = 'windows_advfirewall'
  CMD_PREFIX = 'netsh advfirewall firewall '

  # Logging:
  # netsh advfirewall>set allprofiles logging allowedconnections enable"
  # netsh advfirewall>set allprofiles logging droppedconnections enable"

  # 'in' or 'out'
  _DIR_ATOM = string.Template('dir=${dir}')
  # 'local' or 'remote'
  _ADDR_ATOM = string.Template('${dir}ip=${addr}')
  _PORT_ATOM = string.Template('${dir}port=${port}')
  # any | Integer | icmpv4 | icmpv6 | icmpv4:type,code | icmpv6:type,code
  # | tcp | udp
  _PROTO_ATOM = string.Template('protocol=${protocol}')
  # 'allow' or 'block'
  _ACTION_ATOM = string.Template('action=${action}')

  _RULE_FORMAT = string.Template('add rule name=${name} enable=yes '
                                 'interfacetype=any ${atoms}')

  _ACTION_TABLE = {
      'accept': 'allow',
      'deny': 'block',
      'reject': 'block',
      }

  def _HandleIcmpTypes(self, icmp_types, protocols):
    # advfirewall actually puts this in the protocol spec, eg.:
    # icmpv4 | icmpv6 | icmpv4:type,code | icmpv6:type,code
    types = ['']
    if icmp_types:
      types = self.NormalizeIcmpTypes(self.term.icmp_type, protocols, self.af)
      # NormalizeIcmpTypes enforces this the af/ip version match:
      icmp_prefix = 'icmpv4'
      if self.af == 'inet6':
        icmp_prefix = 'icmpv6'

      if types:
        protocols = []
        for typ in types:
          protocols.append('%s:%s,any' % (icmp_prefix, typ))
        types = ['']

    # fixup for icmp v4
    for i in range(len(protocols)):
      if protocols[i] == 'icmp':
        protocols[i] = 'icmpv4'

    return (types, protocols)

  def _HandlePorts(self, src_ports, dst_ports):
    return ([self._ComposePortString(src_ports)],
            [self._ComposePortString(dst_ports)])

  def _CartesianProduct(self, src_addr, dst_addr, protocol, unused_icmp_types,
                        src_port, dst_port, ret_str):
    # At least advfirewall supports port ranges, unlike windows ipsec,
    # so the src and dst port lists will always be one element long.
    for saddr in src_addr:
      for daddr in dst_addr:
        for proto in protocol:
          ret_str.append(self._ComposeRule(
              saddr, daddr, proto, src_port[0], dst_port[0],
              self.term.action[0]))

  def _ComposeRule(self, srcaddr, dstaddr, proto, srcport, dstport, action):
    """Convert the given parameters into a netsh add rule string."""
    atoms = []
    src_label = 'local'
    dst_label = 'remote'

    # We assume a default direction of OUT, but if it's IN, the Windows
    # advfirewall changes around the remote and local labels.
    if 'in' == self.filter.lower():
      src_label = 'remote'
      dst_label = 'local'

    atoms.append(self._DIR_ATOM.substitute(dir=self.filter))

    if srcaddr.prefixlen == 0:
      atoms.append(self._ADDR_ATOM.substitute(dir=src_label, addr='any'))
    else:
      atoms.append(self._ADDR_ATOM.substitute(dir=src_label, addr=str(srcaddr)))

    if dstaddr.prefixlen == 0:
      atoms.append(self._ADDR_ATOM.substitute(dir=dst_label, addr='any'))
    else:
      atoms.append(self._ADDR_ATOM.substitute(dir=dst_label, addr=str(dstaddr)))

    if srcport:
      atoms.append(self._PORT_ATOM.substitute(dir=src_label, port=srcport))
    if dstport:
      atoms.append(self._PORT_ATOM.substitute(dir=dst_label, port=dstport))

    if proto:
      if proto == 'vrrp':
        proto = '112'
      elif proto == 'ah':
        proto = '51'
      elif proto == 'hopopt':
        proto = '0'
      atoms.append(self._PROTO_ATOM.substitute(protocol=proto))

    atoms.append(self._ACTION_ATOM.substitute(
        action=self._ACTION_TABLE[action]))

    return (self.CMD_PREFIX +
            self._RULE_FORMAT.substitute(
                name=self.term_name, atoms=' '.join(atoms)))

  def _ComposePortString(self, ports):
    """Convert the list of ports tuples into a multiport range string."""
    if not ports:
      return ''

    multiports = []
    for (start, end) in ports:
      if start == end:
        multiports.append(str(start))
      else:
        multiports.append('-'.join([str(start), str(end)]))
    return ','.join(multiports)


class WindowsAdvFirewall(windows.WindowsGenerator):
  """Generates filters and terms from provided policy object."""

  _PLATFORM = 'windows_advfirewall'
  _TERM = Term


# capirca/lib/windows_ipsec.py

# Copyright 2016 Google 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.
#
"""Windows IP security policy generator."""


# pylint: disable=g-importing-member
from string import Template

from absl import logging
from capirca.lib import aclgenerator
from capirca.lib import windows


class Term(windows.Term):
  """Generate windows IP security policy terms."""

  _PLATFORM = 'windows_ipsec'
  CMD_PREFIX = 'netsh ipsec static add '

  # Windows IPSec Policy (which actually isn't limit to IPSec proper, as you
  # might expect) is structured such that you create:
  #   * One policy (more can be defined, but only one active)
  #   * One or more filter lists, which is similar to an IP chain (but does not
  #     support action:: next)
  #   * A filter, which is the matcher portion of a term
  #   * A filteraction, which is the action to perform when a a filter is
  #     matched.  Not that the filter action does not support logging, logging
  #     is associated with a auditpol filterlist.
  #   * a rule, which links a policy, filter list, and filter action.

  # Logging:
  # auditpol /set /subcategory:"Filtering Platform Packet Drop" /success:enable
  #   /failure:enable
  # auditpol /set /subcategory:"IPsec Driver" /success:enable /failure:enable
  # auditpol /set /subcategory:"IPsec Main Mode" /success:enable /failure:enable
  # auditpol /set /subcategory:"IPsec Quick Mode" /success:enable
  #   /failure:enable
  # auditpol /set /subcategory:"IPsec Extended Mode" /success:enable
  #   /failure:enable

  _MASK_ATOM = Template('${dir}mask=${mask}')
  _ADDR_ATOM = Template('${dir}addr=${addr} ${mask}')
  _PROTO_ATOM = Template('protocol=${protocol}')
  _PORT_ATOM = Template('${dir}port=${port}')

  # ipsec is not stateful, we need mirrored=yes to get responses:
  _FILTER_FORMAT = Template('filter filterlist=${name} mirrored=yes '
                            '${atoms}')
  _FILTERACTION_FORMAT = Template('filteraction name=${name} '
                                  'action=${action}')
  _FILTERLIST_FORMAT = Template('filterlist name=$name ')
  _RULE_FORMAT = Template('rule name=$name policy=$policy '
                          'filterlist=$filterlist '
                          'filteraction=$filteraction ')
  _COMMENT_FORMAT = Template(': $comment')
  _LIST_SUFFIX = '-list'
  _ACTION_SUFFIX = '-action'

  # filter rules
  _ACTION_TABLE = {
      'accept': 'permit',
      'deny': 'block',
      'reject': 'block',
      }

  def _HandleIcmpTypes(self, icmp_types, protocols):
    if icmp_types:
      raise aclgenerator.UnsupportedFilterError('\n%s %s %s %s' % (
          'icmp types unsupported by', self._PLATFORM,
          '\nError in term:', self.term.name))
    return ([''], protocols)

  def _HandlePorts(self, src_ports, dst_ports):
    # ports = Map the ports in a straight list since multiports aren't supported
    return (self._CollapsePortTuples(src_ports),
            self._CollapsePortTuples(dst_ports))

  def _HandlePreRule(self, ret_str):
    ret_str.append(self._ComposeFilterList())
    ret_str.append(self._ComposeFilterAction(
        self._ACTION_TABLE[self.term.action[0]]))

  def _CartesianProduct(self, src_addr, dst_addr, protocol, unused_icmp_types,
                        src_port, dst_port, ret_str):
    # yup, the full cartesian product... this makes me cry on the inside.
    for saddr in src_addr:
      if saddr.version != 4:
        logging.warning('WARNING: term contains a non IPv4 address %s, '
                        'ignoring element of term %s.', saddr, self.term_name)
        continue

      for daddr in dst_addr:
        if daddr.version != 4:
          logging.warning('WARNING: term contains a non IPv4 address %s, '
                          'ignoring element of term %s.', daddr, self.term_name)
          continue

        for proto in protocol:
          for sport in src_port:
            for dport in dst_port:
              ret_str.append(self._ComposeFilter(saddr.network_address,
                                                 daddr.network_address,
                                                 proto,
                                                 saddr.prefixlen,
                                                 daddr.prefixlen,
                                                 sport,
                                                 dport))

  def _CollapsePortTuples(self, port_tuples):
    ports = ['']
    for tpl in port_tuples:
      if tpl:
        (port_start, port_end) = tpl
        ports = list(range(port_start, port_end+1))
    return ports

  def _ComposeFilterList(self):
    return (self.CMD_PREFIX +
            self._FILTERLIST_FORMAT.substitute(
                name=self.term_name + self._LIST_SUFFIX))

  def _ComposeFilterAction(self, action):
    return (self.CMD_PREFIX +
            self._FILTERACTION_FORMAT.substitute(
                name=self.term_name + self._ACTION_SUFFIX, action=action))

  def _ComposeFilter(self, srcaddr, dstaddr, proto, srcmask, dstmask,
                     srcport, dstport):
    """Convert the given parameters to a netsh filter rule string."""
    atoms = []

    if srcmask == 0:
      atoms.append(self._ADDR_ATOM.substitute(dir='src', addr='any', mask=''))
    else:
      mask_atom = self._MASK_ATOM.substitute(dir='src', mask=srcmask)
      atoms.append(self._ADDR_ATOM.substitute(
          dir='src', addr=srcaddr, mask=mask_atom))

    if dstmask == 0:
      atoms.append(self._ADDR_ATOM.substitute(dir='dst', addr='any', mask=''))
    else:
      mask_atom = self._MASK_ATOM.substitute(dir='dst', mask=dstmask)
      atoms.append(self._ADDR_ATOM.substitute(
          dir='dst', addr=dstaddr, mask=mask_atom))

    if srcport:
      atoms.append(self._PORT_ATOM.substitute(dir='src', port=srcport))
    if dstport:
      atoms.append(self._PORT_ATOM.substitute(dir='dst', port=dstport))

    if proto:
      atoms.append(self._PROTO_ATOM.substitute(protocol=proto))

    return (self.CMD_PREFIX +
            self._FILTER_FORMAT.substitute(
                name=self.term_name + self._LIST_SUFFIX,
                atoms=' '.join(atoms)))

  def ComposeRule(self, policy):
    return (self.CMD_PREFIX +
            self._RULE_FORMAT.substitute(
                name=self.term_name + '-rule', policy=policy,
                filterlist=self.term_name + self._LIST_SUFFIX,
                filteraction=self.term_name + self._ACTION_SUFFIX))


class WindowsIPSec(windows.WindowsGenerator):
  """Generates filters and terms from provided policy object."""

  _PLATFORM = 'windows_ipsec'
  _TERM = Term

  _POLICY_FORMAT = Template('policy name=$name assign=yes')
  _POLICY_SUFFIX = '-policy'

  _GOOD_AFS = ['inet']

  def _BuildTokens(self):
    """Build supported tokens for platform.

    Returns:
      tuple containing both supported tokens and sub tokens
    """
    supported_tokens, supported_sub_tokens = super()._BuildTokens()

    supported_tokens -= {'icmp_type'}
    del supported_sub_tokens['icmp_type']
    return supported_tokens, supported_sub_tokens

  def _HandlePolicyHeader(self, header, target):
    policy_name = header.FilterName(self._PLATFORM) + self._POLICY_SUFFIX
    target.append(Term.CMD_PREFIX +
                  self._POLICY_FORMAT.substitute(name=policy_name) + '\n')

  def _HandleTermFooter(self, header, term, target):
    target.append(
        term.ComposeRule(header.FilterName(self._PLATFORM)) + '\n')


# capirca/lib/newnftables.py
# Copyright 2022 Google 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.
#
"""NFtables policy generator for capirca."""

import collections
import datetime
import logging

from capirca.lib import aclgenerator
from capirca.lib import nacaddr


class Error(Exception):
  """Base error class."""


class TermError(Error):
  """Raised when a term is not valid."""


class HeaderError(Error):
  """Raised when a header is not valid."""


class UnsupportedFilterTypeError(Error):
  """Raised when an unsupported filter type is specified."""


class Term(aclgenerator.Term):
  """Representation of an individual NFT term.

     This is primarily useful for NewNftables.__str__() method.

     Args: term policy.Term object
  """

  _ALLOWED_PROTO_NAME = frozenset([
      'tcp', 'udp', 'icmp', 'esp', 'udp', 'ah', 'comp', 'udplite', 'dccp',
      'sctp', 'icmpv6'
  ])
  _ACTIONS = {
      'accept': 'allow',
      'deny': 'discard',
      'reject': 'say go away to',
      'next': 'pass it onto the next term',
      'reject-with-tcp-rst': 'reset'
  }

  def __init__(self, term, nf_af, nf_hook, verbose=True):
    """Individual instances of a Term for NFtables.

    Args:
      term: Term data.
      nf_af: nftables table type IPv4 only (ip), IPv6 (ip6) or dual-stack
        (inet).
      nf_hook: INPUT or OUTPUT (packet processing/direction of traffic).
      verbose: used for comment handling.
    """
    self.term = term
    self.address_family = nf_af
    self.hook = nf_hook
    self.verbose = verbose

  def CreateAnonymousSet(self, string_elements):
    """Build a nftables anonymous set from some elements.

    Anonymous are formatted using curly braces then some data. These sets are
    bound to a rule, have no specific name and cannot be updated.

    Args:
      string_elements: a list of strings to format.

    Returns:
      formatted string of items as anonymous set.
    """
    nfset = ''
    if len(string_elements) == 1:
      nfset = str(string_elements)
    if len(string_elements) > 1:
      nfset = ', '.join(string_elements)
    return '{{ %s }}' % nfset

  def _RulesetGenerator(self, term):
    """Generate string rules of a given Term.

    Rules are constructed from Terms() and are contained within chains.
    This function generates rules that will be present inside a regular
    (non-base) chain. Each item in list represents a line break for later
    parsing.

    Args:
      term: term data.

    Returns:
      list of strings. Representing a ruleset for later formatting.
    """
    term_ruleset = []
    src_addr_book = self._AddressClassifier(term.source_address)
    dst_addr_book = self._AddressClassifier(term.destination_address)

    def ICMP(nf_family, protocol, icmp_code, src_addr, dst_addr):
      """ICMP Term handling.

      Args:
        nf_family: nftables address family (ip, ip6, inet for mixed)
        protocol: list of protocols for term
        icmp_code: icmp specific option
        src_addr: source networks in dict[nf_family] format.
        dst_addr: destination networks in dict[nf_family] format.

      Returns:
        list of strings containing rule in specific format.
      """

    # COMMENT handling.
    if self.verbose:
      for line in self.term.comment:
        term_ruleset.append('comment "%s"' % line)

    # Protocol handling.
    # TODO(gfm): CL 2 handles protocols.

    # TODO(gfm): Handle rules with network addresses.
    return term_ruleset

  def _AddressClassifier(self, address_to_classify):
    """Organizes network addresses according to IP family in a dict.

    Args:
      address_to_classify: list of network addresses

    Returns:
      dictionary of network addresses classified by AF.
    """
    addresses = collections.defaultdict(list)
    for addr in address_to_classify:
      if addr.version == 4:
        addresses['ip'].append(addr)
      if addr.version == 6:
        addresses['ip6'].append(addr)
    return addresses

  def _Group(self, group):
    """If 1 item return it, else return [ item1 item2 ].

    Args:
      group: a list.  could be a list of strings (protocols) or a list of tuples
        (ports)

    Returns:
      rval: a string surrounded by '[' and ']'
    """

    def _FormatPorts(port):
      if isinstance(port, int):
        return str(port)
      elif port[0] == port[1]:
        return '%d' % port[0]
      else:
        # port range
        return '%d-%d' % (port[0], port[1])

    if len(group) > 1:
      rval = [_FormatPorts(x) for x in group]
    else:
      rval = _FormatPorts(group[0])
    return rval

  def __str__(self):
    """Terms printing function."""

    # Things we need to do in this section:
    # 1) validate term IPv4 or IPv6 = the family of the table its going in.
    # 2)
    # Verify platform specific terms. Skip whole term if platform does not
    # match.
    if self.term.platform:
      if 'newnftables' not in self.term.platform:
        return ''
    if self.term.platform_exclude:
      if 'newnftables' in self.term.platform_exclude:
        return ''

    # Terms printing.
    print('TERM = ', self.term)
    self._RulesetGenerator(self.term)

    # Create nftables IP family dictionaries.
    if self.term.source_address:
      source_addresses = self._AddressClassifier(self.term.source_address)
    if self.term.destination_address:
      destination_address = self._AddressClassifier(
          self.term.destination_address)

    return ''


class NewNftables(aclgenerator.ACLGenerator):
  """A NFtables policy object."""

  _PLATFORM = 'newnftables'
  SUFFIX = '.nft'
  _HEADER_AF = frozenset(('inet', 'inet6', 'mixed'))
  _SUPPORTED_HOOKS = frozenset(('input', 'output'))
  _HOOK_PRIORITY_DEFAULT = 0
  _BASE_CHAIN_PREFIX = 'root'  # TODO(gfm): will be changed.

  _OPTIONAL_SUPPORTED_KEYWORDS = frozenset([
      'expiration',
  ])

  # Below mapping converts capirca HEADER native to nftables table.
  # In Nftables 'inet' contains both IPv4 and IPv6 addresses and rules.
  NF_TABLE_AF_MAP = {'inet': 'ip', 'inet6': 'ip6', 'mixed': 'inet'}

  def _TranslatePolicy(self, pol, exp_info):
    """Translates a Capirca policy file into NFtables specific data structure.

    Reads a POL file, filters for NFTables specific data, parses each term
    and populates the nftables_policies list.

    Args:
      pol: A Policy() object representing a given POL file.
      exp_info: An int that specifies number of weeks until policy expires.

    Raises:
      TermError: Raised when policy term requirements are not met.
    """
    self.nftables_policies = []

    def ProcessHeader(header_options):
      """Capirca policy header processing.

      Args:
        header_options: capirca policy header data (filter_options)

      Raises:
        HeaderError: Raised when the policy header format requirements are not
        met.

      Returns:
        netfilter_family: x. filter_options[0]
        netfilter_hook: x. filter_options[1].lower()
        netfilter_priority: numbers = [x for x in filter_options if x.isdigit()]
        policy_default_action: nftable action to take on unmatched packets.
        verbose: header and term verbosity.
      """
      if len(header_options) < 2:
        raise HeaderError(
            'Invalid header for Nftables. Required fields missing.')
      # First header element should dictate type of policy.
      if header_options[0] not in NewNftables._HEADER_AF:
        raise HeaderError(
            'Invalid address family in header: %s. Supported: %s' %
            (header_options[0], NewNftables._HEADER_AF))
      netfilter_family = self.NF_TABLE_AF_MAP.get(header_options[0])
      policy_default_action = 'drop'
      if 'ACCEPT' in header_options:
        policy_default_action = 'accept'
      netfilter_hook = header_options[1].lower()
      if netfilter_hook not in self._SUPPORTED_HOOKS:
        raise HeaderError(
            '%s is not a supported nftables hook. Supported hooks: %s' %
            (netfilter_hook, list(self._SUPPORTED_HOOKS)))
      if len(header_options) >= 2:
        numbers = [x for x in header_options if x.isdigit()]
        if not numbers:
          netfilter_priority = self._HOOK_PRIORITY_DEFAULT
          logging.info(
              'INFO: NFtables priority not specified in header.'
              'Defaulting to %s', self._HOOK_PRIORITY_DEFAULT)
        if len(numbers) == 1:
          # A single integer value is used to set priority.
          netfilter_priority = numbers[0]
        if len(numbers) > 1:
          raise HeaderError('Too many integers in header.')
      verbose = True
      if 'noverbose' in header_options:
        verbose = False
        header_options.remove('noverbose')
      return netfilter_family, netfilter_hook, netfilter_priority, policy_default_action, verbose

    current_date = datetime.date.today()
    exp_info_date = current_date + datetime.timedelta(weeks=exp_info)
    for header, terms in pol.filters:
      if self._PLATFORM not in header.platforms:
        continue
      filter_options = header.FilterOptions('newnftables')
      nf_af, nf_hook, nf_priority, filter_policy_default_action, verbose = ProcessHeader(
          filter_options)

      term_names = set()
      new_terms = []
      for term in terms:
        if term.name in term_names:
          raise TermError('Duplicate term name')
        term_names.add(term.name)
        if term.expiration:
          if term.expiration <= exp_info_date:
            logging.info(
                'INFO: Term %s in policy %s expires '
                'in less than two weeks.', term.name, nf_af)
          if term.expiration <= current_date:
            logging.warning(
                'WARNING: Term %s in policy %s is expired and '
                'will not be rendered.', term.name, nf_af)
            continue
        # Handle address excludes before building nft address book dict.
        for i in term.source_address_exclude:
          term.source_address = nacaddr.RemoveAddressFromList(
              term.source_address, i)
        for i in term.destination_address_exclude:
          term.destination_address = nacaddr.RemoveAddressFromList(
              term.destination_address, i)
        new_terms.append(Term(term, nf_af, nf_hook, verbose))
      self.nftables_policies.append(
          (header, nf_af, nf_hook, nf_priority, filter_policy_default_action,
           verbose, new_terms))

  def __str__(self):
    """Render the policy as Nftables configuration."""
    self.tables = collections.defaultdict(list)
    self.chains = collections.defaultdict(list)
    nft_config = []

    # output = self._BuildTables(self.nftables_policies)
    for (header, nf_af, nf_hook, nf_priority, filter_policy_default_action,
         verbose, new_terms) in self.nftables_policies:
      base_chain_comment = ''
      # Add max character checking on header.comment later if needed.
      if verbose:
        base_chain_comment = header.comment
      self.tables[nf_af].append(
          (nf_hook, nf_priority, filter_policy_default_action,
           base_chain_comment, new_terms))

    for nf_af in self.tables:
      nft_config.append('table %s filtering_policies {' % nf_af)
      for count, elem in enumerate(self.tables[nf_af]):
        base_chain_name = self._BASE_CHAIN_PREFIX + str(count)

        nft_config.append('\tchain %s {' % base_chain_name)
        if str(elem[1]):
          nft_config.append('\t\tcomment "%s"' % elem[0][0])
        nft_config.append('\t\ttype filter hook %s priority %s; policy %s;' %
                          (elem[0], elem[1], elem[2]))
        base_chain_terms = elem[4]
        for valid_term in base_chain_terms:
          # Here we call Term(str)
          term_output = str(valid_term)
          if term_output:
            nft_config.append(term_output)
            print(term_output)  # space then term_rule.
        nft_config.append('\t}')  # chain_end
      nft_config.append('}')  # table_end

    return '\n '.join(nft_config)
