1"""
2Capirca ACL
3===========
4
5Generate ACL (firewall) configuration for network devices.
6
7.. versionadded:: 2017.7.0
8
9:codeauthor: Mircea Ulinic <ping@mirceaulinic.net> & Robert Ankeny <robankeny@google.com>
10:maturity:   new
11:depends:    capirca
12:platform:   unix
13
14Dependencies
15------------
16
17The firewall configuration is generated by Capirca_.
18
19.. _Capirca: https://github.com/google/capirca
20
21To install Capirca, execute: ``pip install capirca``.
22"""
23
24import datetime
25import inspect
26import logging
27import re
28
29import salt.utils.files
30
31log = logging.getLogger(__file__)
32
33
34try:
35    import capirca
36    import capirca.aclgen
37    import capirca.lib.policy
38    import capirca.lib.aclgenerator
39
40    HAS_CAPIRCA = True
41except ImportError:
42    HAS_CAPIRCA = False
43
44
45# ------------------------------------------------------------------------------
46# module properties
47# ------------------------------------------------------------------------------
48
49__virtualname__ = "capirca"
50__proxyenabled__ = ["*"]
51# allow any proxy type
52
53# ------------------------------------------------------------------------------
54# property functions
55# ------------------------------------------------------------------------------
56
57
58def __virtual__():
59    """
60    This module requires at least Capirca to work.
61    """
62    if HAS_CAPIRCA:
63        return __virtualname__
64    else:
65        return (False, "The capirca module (capirca_acl) cannot be loaded.")
66
67
68# ------------------------------------------------------------------------------
69# module globals
70# ------------------------------------------------------------------------------
71
72
73# define the default values for all possible term fields
74# we could also extract them from the `policy` module, inspecting the `Policy`
75# class, but that might be overkill & it would make the code less obvious.
76# we can revisit this later if necessary.
77
78_TERM_FIELDS = {
79    "action": [],
80    "address": [],
81    "address_exclude": [],
82    "comment": [],
83    "counter": None,
84    "expiration": None,
85    "destination_address": [],
86    "destination_address_exclude": [],
87    "destination_port": [],
88    "destination_prefix": [],
89    "forwarding_class": [],
90    "forwarding_class_except": [],
91    "logging": [],
92    "log_name": None,
93    "loss_priority": None,
94    "option": [],
95    "owner": None,
96    "policer": None,
97    "port": [],
98    "precedence": [],
99    "principals": [],
100    "protocol": [],
101    "protocol_except": [],
102    "qos": None,
103    "pan_application": [],
104    "routing_instance": None,
105    "source_address": [],
106    "source_address_exclude": [],
107    "source_port": [],
108    "source_prefix": [],
109    "verbatim": [],
110    "packet_length": None,
111    "fragment_offset": None,
112    "hop_limit": None,
113    "icmp_type": [],
114    "icmp_code": None,
115    "ether_type": [],
116    "traffic_class_count": None,
117    "traffic_type": [],
118    "translated": False,
119    "dscp_set": None,
120    "dscp_match": [],
121    "dscp_except": [],
122    "next_ip": None,
123    "flexible_match_range": [],
124    "source_prefix_except": [],
125    "destination_prefix_except": [],
126    "vpn": None,
127    "source_tag": [],
128    "destination_tag": [],
129    "source_interface": None,
130    "destination_interface": None,
131    "platform": [],
132    "platform_exclude": [],
133    "timeout": None,
134    "flattened": False,
135    "flattened_addr": None,
136    "flattened_saddr": None,
137    "flattened_daddr": None,
138    "priority": None,
139    "ttl": None,
140}
141
142# IP-type fields
143# when it comes to IP fields, Capirca does not ingest raw text
144# but they need to be converted to `nacaddr.IP`
145# this pre-processing is done in `_clean_term_opts`
146_IP_FILEDS = [
147    "source_address",
148    "source_address_exclude",
149    "destination_address",
150    "address",
151    "address_exclude",
152    "flattened_addr",
153    "flattened_saddr",
154    "flattened_daddr",
155    "next_ip",
156]
157
158_SERVICES = {}
159
160# ------------------------------------------------------------------------------
161# helper functions -- will not be exported
162# ------------------------------------------------------------------------------
163
164
165if HAS_CAPIRCA:
166    _TempTerm = capirca.lib.policy.Term
167
168    def _add_object(self, obj):
169        return
170
171    setattr(_TempTerm, "AddObject", _add_object)
172    dumy_term = _TempTerm(None)
173    for item in dir(dumy_term):
174        if hasattr(item, "__func__") or item.startswith("_") or item != item.lower():
175            continue
176        _TERM_FIELDS[item] = getattr(dumy_term, item)
177
178    class _Policy(capirca.lib.policy.Policy):
179        """
180        Extending the Capirca Policy class to allow inserting custom filters.
181        """
182
183        def __init__(self):
184            self.filters = []
185            self.filename = ""
186
187    class _Term(capirca.lib.policy.Term):
188        """
189        Extending the Capirca Term class to allow setting field valued on the fly.
190        """
191
192        def __init__(self):
193            for field, default in _TERM_FIELDS.items():
194                setattr(self, field, default)
195
196
197def _import_platform_generator(platform):
198    """
199    Given a specific platform (under the Capirca conventions),
200    return the generator class.
201    The generator class is identified looking under the <platform> module
202    for a class inheriting the `ACLGenerator` class.
203    """
204    log.debug("Using platform: %s", platform)
205    for mod_name, mod_obj in inspect.getmembers(capirca.aclgen):
206        if mod_name == platform and inspect.ismodule(mod_obj):
207            for plat_obj_name, plat_obj in inspect.getmembers(
208                mod_obj
209            ):  # pylint: disable=unused-variable
210                if inspect.isclass(plat_obj) and issubclass(
211                    plat_obj, capirca.lib.aclgenerator.ACLGenerator
212                ):
213                    log.debug("Identified Capirca class %s for %s", plat_obj, platform)
214                    return plat_obj
215    log.error("Unable to identify any Capirca plaform class for %s", platform)
216
217
218def _get_services_mapping():
219    """
220    Build a map of services based on the IANA assignment list:
221    http://www.iana.org/assignments/port-numbers
222
223    It will load the /etc/services file and will build the mapping on the fly,
224    similar to the Capirca's SERVICES file:
225    https://github.com/google/capirca/blob/master/def/SERVICES.svc
226
227    As this module is be available on Unix systems only,
228    we'll read the services from /etc/services.
229    In the worst case, the user will not be able to specify the
230    services shortcut and they will need to specify the protocol / port combination
231    using the source_port / destination_port & protocol fields.
232    """
233    if _SERVICES:
234        return _SERVICES
235    services_txt = ""
236    try:
237        with salt.utils.files.fopen("/etc/services", "r") as srv_f:
238            services_txt = salt.utils.stringutils.to_unicode(srv_f.read())
239    except OSError as ioe:
240        log.error("Unable to read from /etc/services:")
241        log.error(ioe)
242        return _SERVICES  # no mapping possible, sorry
243        # will return the default mapping
244    service_rgx = re.compile(r"^([a-zA-Z0-9-]+)\s+(\d+)\/(tcp|udp)(.*)$")
245    for line in services_txt.splitlines():
246        service_rgx_s = service_rgx.search(line)
247        if service_rgx_s and len(service_rgx_s.groups()) == 4:
248            srv_name, port, protocol, _ = service_rgx_s.groups()
249            if srv_name not in _SERVICES:
250                _SERVICES[srv_name] = {"port": [], "protocol": []}
251            try:
252                _SERVICES[srv_name]["port"].append(int(port))
253            except ValueError as verr:
254                log.error(verr)
255                log.error("Did not read that properly:")
256                log.error(line)
257                log.error(
258                    "Please report the above error: %s does not seem a valid port"
259                    " value!",
260                    port,
261                )
262            _SERVICES[srv_name]["protocol"].append(protocol)
263    return _SERVICES
264
265
266def _translate_port(port):
267    """
268    Look into services and return the port value using the
269    service name as lookup value.
270    """
271    services = _get_services_mapping()
272    if port in services and services[port]["port"]:
273        return services[port]["port"][0]
274    return port
275
276
277def _make_it_list(dict_, field_name, value):
278    """
279    Return the object list.
280    """
281    prev_value = []
282    # firsly we'll collect the prev value
283    if field_name in dict_:
284        prev_value = dict_[field_name]
285    if value is None:
286        return prev_value
287    elif isinstance(value, (tuple, list)):
288        # other type of iterables
289        if field_name in ("source_port", "destination_port"):
290            # port fields are more special
291            # they can either be a list of integers, either a list of tuples
292            # list of integers = a list of ports
293            # list of tuples = a list of ranges,
294            # e.g.: [(1000, 2000), (3000, 4000)] means the 1000-2000 and 3000-4000 ranges
295            portval = []
296            for port in value:
297                if not isinstance(port, (tuple, list)):
298                    # to make sure everything is consistent,
299                    # we'll transform indivitual ports into tuples
300                    # thus an individual port e.g. 1000 will be transormed into the port range 1000-1000
301                    # which is the equivalent
302                    # but assures consistency for the Capirca parser
303                    portval.append((port, port))
304                else:
305                    portval.append(port)
306            translated_portval = []
307            # and the ports sent as string, e.g. ntp instead of 123
308            # needs to be translated
309            # again, using the same /etc/services
310            for port_start, port_end in portval:
311                if not isinstance(port_start, int):
312                    port_start = _translate_port(port_start)
313                if not isinstance(port_end, int):
314                    port_end = _translate_port(port_end)
315                translated_portval.append((port_start, port_end))
316            return list(set(prev_value + translated_portval))
317        return list(set(prev_value + list(value)))
318    if field_name in ("source_port", "destination_port"):
319        if not isinstance(value, int):
320            value = _translate_port(value)
321        return list(set(prev_value + [(value, value)]))  # a list of tuples
322    # anything else will be enclosed in a list-type
323    return list(set(prev_value + [value]))
324
325
326def _clean_term_opts(term_opts):
327    """
328    Cleanup the term opts:
329
330    - strip Null and empty valuee, defaulting their value to their base definition from _TERM_FIELDS
331    - convert to `nacaddr.IP` fields from `_IP_FILEDS`
332    - create lists for those fields requiring it
333    """
334    clean_opts = {}
335    _services = _get_services_mapping()
336    for field, value in term_opts.items():
337        # firstly we'll process special fields like source_service or destination_services
338        # which will inject values directly in the source or destination port and protocol
339        if field == "source_service" and value:
340            if isinstance(value, str):
341                value = _make_it_list(clean_opts, field, value)
342            log.debug("Processing special source services:")
343            log.debug(value)
344            for service in value:
345                if service and service in _services:
346                    # if valid source_service
347                    # take the port and protocol values from the global and inject in the term config
348                    clean_opts["source_port"] = _make_it_list(
349                        clean_opts, "source_port", _services[service]["port"]
350                    )
351                    clean_opts["protocol"] = _make_it_list(
352                        clean_opts, "protocol", _services[service]["protocol"]
353                    )
354            log.debug(
355                "Built source_port field, after processing special source services:"
356            )
357            log.debug(clean_opts.get("source_port"))
358            log.debug("Built protocol field, after processing special source services:")
359            log.debug(clean_opts.get("protocol"))
360        elif field == "destination_service" and value:
361            if isinstance(value, str):
362                value = _make_it_list(clean_opts, field, value)
363            log.debug("Processing special destination services:")
364            log.debug(value)
365            for service in value:
366                if service and service in _services:
367                    # if valid destination_service
368                    # take the port and protocol values from the global and inject in the term config
369                    clean_opts["destination_port"] = _make_it_list(
370                        clean_opts, "destination_port", _services[service]["port"]
371                    )
372                    clean_opts["protocol"] = _make_it_list(
373                        clean_opts, "protocol", _services[service]["protocol"]
374                    )
375            log.debug(
376                "Built source_port field, after processing special destination"
377                " services:"
378            )
379            log.debug(clean_opts.get("destination_service"))
380            log.debug(
381                "Built protocol field, after processing special destination services:"
382            )
383            log.debug(clean_opts.get("protocol"))
384        # not a special field, but it has to be a valid one
385        elif field in _TERM_FIELDS and value and value != _TERM_FIELDS[field]:
386            # if not a special field type
387            if isinstance(_TERM_FIELDS[field], list):
388                value = _make_it_list(clean_opts, field, value)
389            if field in _IP_FILEDS:
390                # IP-type fields need to be transformed
391                ip_values = []
392                for addr in value:
393                    ip_values.append(capirca.lib.policy.nacaddr.IP(addr))
394                value = ip_values[:]
395            clean_opts[field] = value
396    return clean_opts
397
398
399def _lookup_element(lst, key):
400    """
401    Find an dictionary in a list of dictionaries, given its main key.
402    """
403    if not lst:
404        return {}
405    for ele in lst:
406        if not ele or not isinstance(ele, dict):
407            continue
408        if key in ele:
409            return ele[key]
410    return {}
411
412
413def _get_pillar_cfg(pillar_key, pillarenv=None, saltenv=None):
414    """
415    Retrieve the pillar data from the right environment.
416    """
417    pillar_cfg = __salt__["pillar.get"](
418        pillar_key, pillarenv=pillarenv, saltenv=saltenv
419    )
420    return pillar_cfg
421
422
423def _cleanup(lst):
424    """
425    Return a list of non-empty dictionaries.
426    """
427    clean = []
428    for ele in lst:
429        if ele and isinstance(ele, dict):
430            clean.append(ele)
431    return clean
432
433
434def _merge_list_of_dict(first, second, prepend=True):
435    """
436    Merge lists of dictionaries.
437    Each element of the list is a dictionary having one single key.
438    That key is then used as unique lookup.
439    The first element list has higher priority than the second.
440    When there's an overlap between the two lists,
441    it won't change the position, but the content.
442    """
443    first = _cleanup(first)
444    second = _cleanup(second)
445    if not first and not second:
446        return []
447    if not first and second:
448        return second
449    if first and not second:
450        return first
451    # Determine overlaps
452    # So we don't change the position of the existing terms/filters
453    overlaps = []
454    merged = []
455    appended = []
456    for ele in first:
457        if _lookup_element(second, next(iter(ele))):
458            overlaps.append(ele)
459        elif prepend:
460            merged.append(ele)
461        elif not prepend:
462            appended.append(ele)
463    for ele in second:
464        ele_key = next(iter(ele))
465        if _lookup_element(overlaps, ele_key):
466            # If there's an overlap, get the value from the first
467            # But inserted into the right position
468            ele_val_first = _lookup_element(first, ele_key)
469            merged.append({ele_key: ele_val_first})
470        else:
471            merged.append(ele)
472    if not prepend:
473        merged.extend(appended)
474    return merged
475
476
477def _get_term_object(
478    filter_name,
479    term_name,
480    pillar_key="acl",
481    pillarenv=None,
482    saltenv=None,
483    merge_pillar=True,
484    **term_fields
485):
486    """
487    Return an instance of the ``_Term`` class given the term options.
488    """
489    log.debug("Generating config for term %s under filter %s", term_name, filter_name)
490    term = _Term()
491    term.name = term_name
492    term_opts = {}
493    if merge_pillar:
494        term_opts = get_term_pillar(
495            filter_name,
496            term_name,
497            pillar_key=pillar_key,
498            saltenv=saltenv,
499            pillarenv=pillarenv,
500        )
501        log.debug("Merging with pillar data:")
502        log.debug(term_opts)
503        term_opts = _clean_term_opts(term_opts)
504        log.debug("Cleaning up pillar data:")
505        log.debug(term_opts)
506    log.debug("Received processing opts:")
507    log.debug(term_fields)
508    log.debug("Cleaning up processing opts:")
509    term_fields = _clean_term_opts(term_fields)
510    log.debug(term_fields)
511    log.debug("Final term opts:")
512    term_opts.update(term_fields)
513    log.debug(term_fields)
514    for field, value in term_opts.items():
515        # setting the field attributes to the term instance of _Term
516        setattr(term, field, value)
517    log.debug("Term config:")
518    log.debug(str(term))
519    return term
520
521
522def _get_policy_object(
523    platform,
524    filters=None,
525    pillar_key="acl",
526    pillarenv=None,
527    saltenv=None,
528    merge_pillar=True,
529):
530    """
531    Return an instance of the ``_Policy`` class given the filters config.
532    """
533    policy = _Policy()
534    policy_filters = []
535    if not filters:
536        filters = []
537    for filter_ in filters:
538        if not filter_ or not isinstance(filter_, dict):
539            continue  # go to the next filter
540        filter_name, filter_config = next(iter(filter_.items()))
541        header = capirca.lib.policy.Header()  # same header everywhere
542        target_opts = [platform, filter_name]
543        filter_options = filter_config.pop("options", None)
544        if filter_options:
545            filter_options = _make_it_list({}, filter_name, filter_options)
546            # make sure the filter options are sent as list
547            target_opts.extend(filter_options)
548        target = capirca.lib.policy.Target(target_opts)
549        header.AddObject(target)
550        filter_terms = []
551        for term_ in filter_config.get("terms", []):
552            if term_ and isinstance(term_, dict):
553                term_name, term_fields = next(iter(term_.items()))
554                term = _get_term_object(
555                    filter_name,
556                    term_name,
557                    pillar_key=pillar_key,
558                    pillarenv=pillarenv,
559                    saltenv=saltenv,
560                    merge_pillar=merge_pillar,
561                    **term_fields
562                )
563            filter_terms.append(term)
564        policy_filters.append((header, filter_terms))
565    policy.filters = policy_filters
566    log.debug("Policy config:")
567    log.debug(str(policy))
568    platform_generator = _import_platform_generator(platform)
569    policy_config = platform_generator(policy, 2)
570    log.debug("Generating policy config for %s:", platform)
571    log.debug(str(policy_config))
572    return policy_config
573
574
575def _revision_tag(
576    text,
577    revision_id=None,
578    revision_no=None,
579    revision_date=True,
580    revision_date_format="%Y/%m/%d",
581):
582    """
583    Refactor revision tag comments.
584    Capirca generates the filter text having the following tag keys:
585
586    - $Id:$
587    - $Revision:$
588    - $Date:$
589
590    This function goes through all the config lines and replaces
591    those tags with the content requested by the user.
592    If a certain value is not provided, the corresponding tag will be stripped.
593    """
594    timestamp = datetime.datetime.now().strftime(revision_date_format)
595    new_text = []
596    for line in text.splitlines():
597        if "$Id:$" in line:
598            if not revision_id:  # if no explicit revision ID required
599                continue  # jump to next line, ignore this one
600            line = line.replace("$Id:$", "$Id: {rev_id} $".format(rev_id=revision_id))
601        if "$Revision:$" in line:
602            if not revision_no:  # if no explicit revision number required
603                continue  # jump to next line, ignore this one
604            line = line.replace(
605                "$Revision:$", "$Revision: {rev_no} $".format(rev_no=revision_no)
606            )
607        if "$Date:$" in line:
608            if not revision_date:
609                continue  # jump
610            line = line.replace("$Date:$", "$Date: {ts} $".format(ts=timestamp))
611        new_text.append(line)
612    return "\n".join(new_text)
613
614
615# ------------------------------------------------------------------------------
616# callable functions
617# ------------------------------------------------------------------------------
618
619
620def get_term_config(
621    platform,
622    filter_name,
623    term_name,
624    filter_options=None,
625    pillar_key="acl",
626    pillarenv=None,
627    saltenv=None,
628    merge_pillar=True,
629    revision_id=None,
630    revision_no=None,
631    revision_date=True,
632    revision_date_format="%Y/%m/%d",
633    source_service=None,
634    destination_service=None,
635    **term_fields
636):
637    """
638    Return the configuration of a single policy term.
639
640    platform
641        The name of the Capirca platform.
642
643    filter_name
644        The name of the policy filter.
645
646    term_name
647        The name of the term.
648
649    filter_options
650        Additional filter options. These options are platform-specific.
651        E.g.: ``inet6``, ``bridge``, ``object-group``,
652        See the complete list of options_.
653
654        .. _options: https://github.com/google/capirca/wiki/Policy-format#header-section
655
656    pillar_key: ``acl``
657        The key in the pillar containing the default attributes values. Default: ``acl``.
658        If the pillar contains the following structure:
659
660        .. code-block:: yaml
661
662            firewall:
663              - my-filter:
664                  terms:
665                    - my-term:
666                        source_port: 1234
667                        source_address:
668                            - 1.2.3.4/32
669                            - 5.6.7.8/32
670
671        The ``pillar_key`` field would be specified as ``firewall``.
672
673    pillarenv
674        Query the master to generate fresh pillar data on the fly,
675        specifically from the requested pillar environment.
676
677    saltenv
678        Included only for compatibility with
679        :conf_minion:`pillarenv_from_saltenv`, and is otherwise ignored.
680
681    merge_pillar: ``True``
682        Merge the CLI variables with the pillar. Default: ``True``.
683
684    revision_id
685        Add a comment in the term config having the description for the changes applied.
686
687    revision_no
688        The revision count.
689
690    revision_date: ``True``
691        Boolean flag: display the date when the term configuration was generated. Default: ``True``.
692
693    revision_date_format: ``%Y/%m/%d``
694        The date format to be used when generating the perforce data. Default: ``%Y/%m/%d`` (<year>/<month>/<day>).
695
696    source_service
697        A special service to choose from. This is a helper so the user is able to
698        select a source just using the name, instead of specifying a source_port and protocol.
699
700        As this module is available on Unix platforms only,
701        it reads the IANA_ port assignment from ``/etc/services``.
702
703        If the user requires additional shortcuts to be referenced, they can add entries under ``/etc/services``,
704        which can be managed using the :mod:`file state <salt.states.file>`.
705
706        .. _IANA: http://www.iana.org/assignments/port-numbers
707
708    destination_service
709        A special service to choose from. This is a helper so the user is able to
710        select a source just using the name, instead of specifying a destination_port and protocol.
711        Allows the same options as ``source_service``.
712
713    term_fields
714        Term attributes.
715        To see what fields are supported, please consult the list of supported keywords_.
716        Some platforms have few other optional_ keywords.
717
718        .. _keywords: https://github.com/google/capirca/wiki/Policy-format#keywords
719        .. _optional: https://github.com/google/capirca/wiki/Policy-format#optionally-supported-keywords
720
721    .. note::
722        The following fields are accepted:
723
724        - action
725        - address
726        - address_exclude
727        - comment
728        - counter
729        - expiration
730        - destination_address
731        - destination_address_exclude
732        - destination_port
733        - destination_prefix
734        - forwarding_class
735        - forwarding_class_except
736        - logging
737        - log_name
738        - loss_priority
739        - option
740        - policer
741        - port
742        - precedence
743        - principals
744        - protocol
745        - protocol_except
746        - qos
747        - pan_application
748        - routing_instance
749        - source_address
750        - source_address_exclude
751        - source_port
752        - source_prefix
753        - verbatim
754        - packet_length
755        - fragment_offset
756        - hop_limit
757        - icmp_type
758        - ether_type
759        - traffic_class_count
760        - traffic_type
761        - translated
762        - dscp_set
763        - dscp_match
764        - dscp_except
765        - next_ip
766        - flexible_match_range
767        - source_prefix_except
768        - destination_prefix_except
769        - vpn
770        - source_tag
771        - destination_tag
772        - source_interface
773        - destination_interface
774        - flattened
775        - flattened_addr
776        - flattened_saddr
777        - flattened_daddr
778        - priority
779
780    .. note::
781        The following fields can be also a single value and a list of values:
782
783        - action
784        - address
785        - address_exclude
786        - comment
787        - destination_address
788        - destination_address_exclude
789        - destination_port
790        - destination_prefix
791        - forwarding_class
792        - forwarding_class_except
793        - logging
794        - option
795        - port
796        - precedence
797        - principals
798        - protocol
799        - protocol_except
800        - pan_application
801        - source_address
802        - source_address_exclude
803        - source_port
804        - source_prefix
805        - verbatim
806        - icmp_type
807        - ether_type
808        - traffic_type
809        - dscp_match
810        - dscp_except
811        - flexible_match_range
812        - source_prefix_except
813        - destination_prefix_except
814        - source_tag
815        - destination_tag
816        - source_service
817        - destination_service
818
819        Example: ``destination_address`` can be either defined as:
820
821        .. code-block:: yaml
822
823            destination_address: 172.17.17.1/24
824
825        or as a list of destination IP addresses:
826
827        .. code-block:: yaml
828
829            destination_address:
830                - 172.17.17.1/24
831                - 172.17.19.1/24
832
833        or a list of services to be matched:
834
835        .. code-block:: yaml
836
837            source_service:
838                - ntp
839                - snmp
840                - ldap
841                - bgpd
842
843    .. note::
844        The port fields ``source_port`` and ``destination_port`` can be used as above to select either
845        a single value, either a list of values, but also they can select port ranges. Example:
846
847        .. code-block:: yaml
848
849            source_port:
850                - [1000, 2000]
851                - [3000, 4000]
852
853        With the configuration above, the user is able to select the 1000-2000 and 3000-4000 source port ranges.
854
855    CLI Example:
856
857    .. code-block:: bash
858
859        salt '*' capirca.get_term_config arista filter-name term-name source_address=1.2.3.4 destination_address=5.6.7.8 action=accept
860
861    Output Example:
862
863    .. code-block:: text
864
865        ! $Date: 2017/03/22 $
866        no ip access-list filter-name
867        ip access-list filter-name
868         remark term-name
869         permit ip host 1.2.3.4 host 5.6.7.8
870        exit
871    """
872    terms = []
873    term = {term_name: {}}
874    term[term_name].update(term_fields)
875    term[term_name].update(
876        {
877            "source_service": _make_it_list({}, "source_service", source_service),
878            "destination_service": _make_it_list(
879                {}, "destination_service", destination_service
880            ),
881        }
882    )
883    terms.append(term)
884    if not filter_options:
885        filter_options = []
886    return get_filter_config(
887        platform,
888        filter_name,
889        filter_options=filter_options,
890        terms=terms,
891        pillar_key=pillar_key,
892        pillarenv=pillarenv,
893        saltenv=saltenv,
894        merge_pillar=merge_pillar,
895        only_lower_merge=True,
896        revision_id=revision_id,
897        revision_no=revision_no,
898        revision_date=revision_date,
899        revision_date_format=revision_date_format,
900    )
901
902
903def get_filter_config(
904    platform,
905    filter_name,
906    filter_options=None,
907    terms=None,
908    prepend=True,
909    pillar_key="acl",
910    pillarenv=None,
911    saltenv=None,
912    merge_pillar=True,
913    only_lower_merge=False,
914    revision_id=None,
915    revision_no=None,
916    revision_date=True,
917    revision_date_format="%Y/%m/%d",
918):
919    """
920    Return the configuration of a policy filter.
921
922    platform
923        The name of the Capirca platform.
924
925    filter_name
926        The name of the policy filter.
927
928    filter_options
929        Additional filter options. These options are platform-specific.
930        See the complete list of options_.
931
932        .. _options: https://github.com/google/capirca/wiki/Policy-format#header-section
933
934    terms
935        List of terms for this policy filter.
936        If not specified or empty, will try to load the configuration from the pillar,
937        unless ``merge_pillar`` is set as ``False``.
938
939    prepend: ``True``
940        When ``merge_pillar`` is set as ``True``, the final list of terms generated by merging
941        the terms from ``terms`` with those defined in the pillar (if any): new terms are prepended
942        at the beginning, while existing ones will preserve the position. To add the new terms
943        at the end of the list, set this argument to ``False``.
944
945    pillar_key: ``acl``
946        The key in the pillar containing the default attributes values. Default: ``acl``.
947
948    pillarenv
949        Query the master to generate fresh pillar data on the fly,
950        specifically from the requested pillar environment.
951
952    saltenv
953        Included only for compatibility with
954        :conf_minion:`pillarenv_from_saltenv`, and is otherwise ignored.
955
956    merge_pillar: ``True``
957        Merge the CLI variables with the pillar. Default: ``True``.
958
959    only_lower_merge: ``False``
960        Specify if it should merge only the terms fields. Otherwise it will try
961        to merge also filters fields. Default: ``False``.
962
963    revision_id
964        Add a comment in the filter config having the description for the changes applied.
965
966    revision_no
967        The revision count.
968
969    revision_date: ``True``
970        Boolean flag: display the date when the filter configuration was generated. Default: ``True``.
971
972    revision_date_format: ``%Y/%m/%d``
973        The date format to be used when generating the perforce data. Default: ``%Y/%m/%d`` (<year>/<month>/<day>).
974
975    CLI Example:
976
977    .. code-block:: bash
978
979        salt '*' capirca.get_filter_config ciscoxr my-filter pillar_key=netacl
980
981    Output Example:
982
983    .. code-block:: text
984
985        ! $Id:$
986        ! $Date:$
987        ! $Revision:$
988        no ipv4 access-list my-filter
989        ipv4 access-list my-filter
990         remark $Id:$
991         remark my-term
992         deny ipv4 any eq 1234 any
993         deny ipv4 any eq 1235 any
994         remark my-other-term
995         permit tcp any range 5678 5680 any
996        exit
997
998    The filter configuration has been loaded from the pillar, having the following structure:
999
1000    .. code-block:: yaml
1001
1002        netacl:
1003          - my-filter:
1004              terms:
1005                - my-term:
1006                    source_port: [1234, 1235]
1007                    action: reject
1008                - my-other-term:
1009                    source_port:
1010                      - [5678, 5680]
1011                    protocol: tcp
1012                    action: accept
1013    """
1014    if not filter_options:
1015        filter_options = []
1016    if not terms:
1017        terms = []
1018    if merge_pillar and not only_lower_merge:
1019        acl_pillar_cfg = _get_pillar_cfg(
1020            pillar_key, saltenv=saltenv, pillarenv=pillarenv
1021        )
1022        filter_pillar_cfg = _lookup_element(acl_pillar_cfg, filter_name)
1023        filter_options = filter_options or filter_pillar_cfg.pop("options", None)
1024        if filter_pillar_cfg:
1025            # Only when it was able to find the filter in the ACL config
1026            pillar_terms = filter_pillar_cfg.get(
1027                "terms", []
1028            )  # No problem if empty in the pillar
1029            terms = _merge_list_of_dict(terms, pillar_terms, prepend=prepend)
1030            # merge the passed variable with the pillar data
1031            # any filter term not defined here, will be appended from the pillar
1032            # new terms won't be removed
1033    filters = []
1034    filters.append(
1035        {
1036            filter_name: {
1037                "options": _make_it_list({}, filter_name, filter_options),
1038                "terms": terms,
1039            }
1040        }
1041    )
1042    return get_policy_config(
1043        platform,
1044        filters=filters,
1045        pillar_key=pillar_key,
1046        pillarenv=pillarenv,
1047        saltenv=saltenv,
1048        merge_pillar=merge_pillar,
1049        only_lower_merge=True,
1050        revision_id=revision_id,
1051        revision_no=revision_no,
1052        revision_date=revision_date,
1053        revision_date_format=revision_date_format,
1054    )
1055
1056
1057def get_policy_config(
1058    platform,
1059    filters=None,
1060    prepend=True,
1061    pillar_key="acl",
1062    pillarenv=None,
1063    saltenv=None,
1064    merge_pillar=True,
1065    only_lower_merge=False,
1066    revision_id=None,
1067    revision_no=None,
1068    revision_date=True,
1069    revision_date_format="%Y/%m/%d",
1070):
1071    """
1072    Return the configuration of the whole policy.
1073
1074    platform
1075        The name of the Capirca platform.
1076
1077    filters
1078        List of filters for this policy.
1079        If not specified or empty, will try to load the configuration from the pillar,
1080        unless ``merge_pillar`` is set as ``False``.
1081
1082    prepend: ``True``
1083        When ``merge_pillar`` is set as ``True``, the final list of filters generated by merging
1084        the filters from ``filters`` with those defined in the pillar (if any): new filters are prepended
1085        at the beginning, while existing ones will preserve the position. To add the new filters
1086        at the end of the list, set this argument to ``False``.
1087
1088    pillar_key: ``acl``
1089        The key in the pillar containing the default attributes values. Default: ``acl``.
1090
1091    pillarenv
1092        Query the master to generate fresh pillar data on the fly,
1093        specifically from the requested pillar environment.
1094
1095    saltenv
1096        Included only for compatibility with
1097        :conf_minion:`pillarenv_from_saltenv`, and is otherwise ignored.
1098
1099    merge_pillar: ``True``
1100        Merge the CLI variables with the pillar. Default: ``True``.
1101
1102    only_lower_merge: ``False``
1103        Specify if it should merge only the filters and terms fields. Otherwise it will try
1104        to merge everything at the policy level. Default: ``False``.
1105
1106    revision_id
1107        Add a comment in the policy config having the description for the changes applied.
1108
1109    revision_no
1110        The revision count.
1111
1112    revision_date: ``True``
1113        Boolean flag: display the date when the policy configuration was generated. Default: ``True``.
1114
1115    revision_date_format: ``%Y/%m/%d``
1116        The date format to be used when generating the perforce data. Default: ``%Y/%m/%d`` (<year>/<month>/<day>).
1117
1118    CLI Example:
1119
1120    .. code-block:: bash
1121
1122        salt '*' capirca.get_policy_config juniper pillar_key=netacl
1123
1124    Output Example:
1125
1126    .. code-block:: text
1127
1128        firewall {
1129            family inet {
1130                replace:
1131                /*
1132                ** $Id:$
1133                ** $Date:$
1134                ** $Revision:$
1135                **
1136                */
1137                filter my-filter {
1138                    term my-term {
1139                        from {
1140                            source-port [ 1234 1235 ];
1141                        }
1142                        then {
1143                            reject;
1144                        }
1145                    }
1146                    term my-other-term {
1147                        from {
1148                            protocol tcp;
1149                            source-port 5678-5680;
1150                        }
1151                        then accept;
1152                    }
1153                }
1154            }
1155        }
1156        firewall {
1157            family inet {
1158                replace:
1159                /*
1160                ** $Id:$
1161                ** $Date:$
1162                ** $Revision:$
1163                **
1164                */
1165                filter my-other-filter {
1166                    interface-specific;
1167                    term dummy-term {
1168                        from {
1169                            protocol [ tcp udp ];
1170                        }
1171                        then {
1172                            reject;
1173                        }
1174                    }
1175                }
1176            }
1177        }
1178
1179    The policy configuration has been loaded from the pillar, having the following structure:
1180
1181    .. code-block:: yaml
1182
1183        netacl:
1184          - my-filter:
1185              options:
1186                - not-interface-specific
1187              terms:
1188                - my-term:
1189                    source_port: [1234, 1235]
1190                    action: reject
1191                - my-other-term:
1192                    source_port:
1193                      - [5678, 5680]
1194                    protocol: tcp
1195                    action: accept
1196          - my-other-filter:
1197              terms:
1198                - dummy-term:
1199                    protocol:
1200                      - tcp
1201                      - udp
1202                    action: reject
1203    """
1204    if not filters:
1205        filters = []
1206    if merge_pillar and not only_lower_merge:
1207        # the pillar key for the policy config is the `pillar_key` itself
1208        policy_pillar_cfg = _get_pillar_cfg(
1209            pillar_key, saltenv=saltenv, pillarenv=pillarenv
1210        )
1211        # now, let's merge everything witht the pillar data
1212        # again, this will not remove any extra filters/terms
1213        # but it will merge with the pillar data
1214        # if this behaviour is not wanted, the user can set `merge_pillar` as `False`
1215        filters = _merge_list_of_dict(filters, policy_pillar_cfg, prepend=prepend)
1216    policy_object = _get_policy_object(
1217        platform,
1218        filters=filters,
1219        pillar_key=pillar_key,
1220        pillarenv=pillarenv,
1221        saltenv=saltenv,
1222        merge_pillar=merge_pillar,
1223    )
1224    policy_text = str(policy_object)
1225    return _revision_tag(
1226        policy_text,
1227        revision_id=revision_id,
1228        revision_no=revision_no,
1229        revision_date=revision_date,
1230        revision_date_format=revision_date_format,
1231    )
1232
1233
1234def get_filter_pillar(filter_name, pillar_key="acl", pillarenv=None, saltenv=None):
1235    """
1236    Helper that can be used inside a state SLS,
1237    in order to get the filter configuration given its name.
1238
1239    filter_name
1240        The name of the filter.
1241
1242    pillar_key
1243        The root key of the whole policy config.
1244
1245    pillarenv
1246        Query the master to generate fresh pillar data on the fly,
1247        specifically from the requested pillar environment.
1248
1249    saltenv
1250        Included only for compatibility with
1251        :conf_minion:`pillarenv_from_saltenv`, and is otherwise ignored.
1252    """
1253    pillar_cfg = _get_pillar_cfg(pillar_key, pillarenv=pillarenv, saltenv=saltenv)
1254    return _lookup_element(pillar_cfg, filter_name)
1255
1256
1257def get_term_pillar(
1258    filter_name, term_name, pillar_key="acl", pillarenv=None, saltenv=None
1259):
1260    """
1261    Helper that can be used inside a state SLS,
1262    in order to get the term configuration given its name,
1263    under a certain filter uniquely identified by its name.
1264
1265    filter_name
1266        The name of the filter.
1267
1268    term_name
1269        The name of the term.
1270
1271    pillar_key: ``acl``
1272        The root key of the whole policy config. Default: ``acl``.
1273
1274    pillarenv
1275        Query the master to generate fresh pillar data on the fly,
1276        specifically from the requested pillar environment.
1277
1278    saltenv
1279        Included only for compatibility with
1280        :conf_minion:`pillarenv_from_saltenv`, and is otherwise ignored.
1281    """
1282    filter_pillar_cfg = get_filter_pillar(
1283        filter_name, pillar_key=pillar_key, pillarenv=pillarenv, saltenv=saltenv
1284    )
1285    term_pillar_cfg = filter_pillar_cfg.get("terms", [])
1286    term_opts = _lookup_element(term_pillar_cfg, term_name)
1287    return term_opts
1288