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