diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 38eae56f..b23fb30b 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,591 +1,591 @@ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json # CodeRabbit configuration for vyos/vyos.vyos Ansible network collection # Docs: https://docs.coderabbit.ai/guides/configure-coderabbit language: en-US early_access: false tone_instructions: > Concise, technical, no filler. Focus on correctness, security, idempotency, and Ansible conventions. Cite file paths and line numbers. reviews: profile: chill request_changes_workflow: false high_level_summary: true - high_level_summary_placeholder: '@coderabbitai summary' - auto_title_placeholder: '@coderabbitai' + high_level_summary_placeholder: "@coderabbitai summary" + auto_title_placeholder: "@coderabbitai" review_status: true poem: false collapse_walkthrough: true changed_files_summary: true sequence_diagrams: false assess_linked_issues: true related_issues: true related_prs: true suggested_labels: false auto_apply_labels: false suggested_reviewers: false auto_review: enabled: true auto_incremental_review: true drafts: false base_branches: - main ignore_title_keywords: - WIP - DO NOT MERGE - Bump path_filters: - - '!**/__pycache__/**' - - '!**/*.pyc' - - '!**/*.egg-info/**' - - '!changelogs/changelog.yaml' - - '!.venv/**' - - '!.collections/**' - - '!.worktrees/**' + - "!**/__pycache__/**" + - "!**/*.pyc" + - "!**/*.egg-info/**" + - "!changelogs/changelog.yaml" + - "!.venv/**" + - "!.collections/**" + - "!.worktrees/**" path_instructions: # ── Global PR hygiene ────────────────────────────────────────────── - - path: '**' + - path: "**" instructions: | This is the vyos.vyos Ansible network collection (namespace=vyos, name=vyos, version=6.0.0). PR titles must follow the format `T{id}: description` referencing a Phorge task at vyos.dev. Every PR must include a changelog fragment in changelogs/fragments/ (YAML, valid keys: major_changes, minor_changes, breaking_changes, deprecated_features, removed_features, security_fixes, bugfixes, known_issues, doc_changes, trivial; plus release_summary as a prelude section). Style: black line-length=100, isort profile=black line_length=100, flake8 max-line-length=120. Do not suggest 88-char wrapping. # ── Module entry points ──────────────────────────────────────────── - - path: 'plugins/modules/vyos_*.py' + - path: "plugins/modules/vyos_*.py" instructions: | Module entry points. Each file must contain three YAML triple-string blocks: DOCUMENTATION, EXAMPLES, and RETURN — this is Ansible's documentation contract, not Python docstrings. Verify: - DOCUMENTATION includes: module, author, short_description, description, version_added, extends_documentation_fragment (vyos.vyos.vyos), options with types and descriptions, and a notes section listing tested VyOS versions. - EXAMPLES has at least one working task per supported state. - RETURN documents all return keys with description, returned, type, and sample. - The module wires argspec, config, and facts classes correctly. - State choices include the full set where applicable: merged, replaced, overridden, deleted, gathered, parsed, rendered. Do not add Python-style docstrings (def-level) to these files — the YAML blocks are the canonical documentation. # ── Argspec (auto-generated) ─────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/argspec/**' + - path: "plugins/module_utils/network/vyos/argspec/**" instructions: | Auto-generated by the Ansible resource module builder. These files carry a "DO NOT EDIT" warning header. Do not suggest modifications to auto-generated argspec files — changes will be overwritten. If the schema needs updating, the resource module builder must regenerate it. Only flag issues if the argument_spec dict has obvious type mismatches or missing required fields that would cause runtime failures. # ── Config classes ───────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/config/**' + - path: "plugins/module_utils/network/vyos/config/**" instructions: | Config builders extending ansible.netcommon ConfigBase or ResourceModule. These generate VyOS CLI commands from desired state. Verify: - execute_module() handles all declared states correctly. - set_config() and _set_config() process gathered facts and desired config without data loss. - Command generation produces valid VyOS CLI syntax (set/delete prefixes, proper quoting of values with spaces). - No silent swallowing of unknown keys — unknown config should raise or warn. - Methods that compare current vs desired state handle empty/None gracefully. Some older config files have auto-generated headers — do not restructure those. # ── Facts classes ────────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/facts/**' + - path: "plugins/module_utils/network/vyos/facts/**" instructions: | Facts classes parse raw VyOS CLI output into structured dicts. Verify: - Regex patterns handle edge cases (missing fields, empty values, quoted strings). - populate() returns a clean dict even when device output is incomplete. - get_device_data() uses the correct show command for the resource. - facts/facts.py FACT_RESOURCE_SUBSETS and FACT_LEGACY_SUBSETS stay in sync with available fact classes. - Legacy facts (facts/legacy/) use run_commands(); resource facts use get_resource_connection(). # ── RM Templates ─────────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/rm_templates/*.py' + - path: "plugins/module_utils/network/vyos/rm_templates/*.py" instructions: | Parser templates mapping structured data to VyOS CLI commands and vice versa. Files with a `_14` suffix target VyOS 1.4+ behavior — do not suggest merging them with the base version. Verify: - _tmplt_* helper functions produce syntactically valid VyOS commands. - Regex patterns in PARSERS list correctly capture all variations of the CLI output (quoted values, optional fields, nested hierarchies). - New templates include both set and delete command generation. - compval/getval paths match the argspec structure. # ── Cliconf plugin ───────────────────────────────────────────────── - - path: 'plugins/cliconf/vyos.py' + - path: "plugins/cliconf/vyos.py" instructions: | Low-level CLI abstraction for VyOS. Handles configure mode, commit, diff, command execution. Changes here affect all modules. Verify: - edit_config() enters configure mode and commits correctly. - get_diff() returns accurate before/after config diffs. - Error handling catches VyOS-specific error patterns (commit failures, invalid commands). - __rpc__ list matches actually implemented methods. # ── Terminal plugin ──────────────────────────────────────────────── - - path: 'plugins/terminal/vyos.py' + - path: "plugins/terminal/vyos.py" instructions: | Terminal prompt detection and initialization. Changes affect connection reliability. Verify regex patterns against actual VyOS prompt formats (configure mode, operational mode, different shell variants). Do not remove existing patterns without testing against all supported VyOS versions. # ── Action plugin ────────────────────────────────────────────────── - - path: 'plugins/action/vyos.py' + - path: "plugins/action/vyos.py" instructions: | Auto-proxies all modules to the device. Must validate network_cli connection type. Symlinks from each module name point here. Keep minimal — logic belongs in config classes, not the action plugin. # ── Changelog fragments ──────────────────────────────────────────── - - path: 'changelogs/fragments/*.{yaml,yml}' + - path: "changelogs/fragments/*.{yaml,yml}" instructions: | Changelog fragments for ansible-changelog. Valid top-level keys: major_changes, minor_changes, breaking_changes, deprecated_features, removed_features, security_fixes, bugfixes, known_issues, doc_changes, trivial. release_summary is a prelude section (one per release). Fragment filename should be descriptive (e.g., fix-bgp-neighbor-timers.yml). Use `trivial` for tooling/housekeeping. Entries should be complete sentences. # ── CI workflows ─────────────────────────────────────────────────── - - path: '.github/workflows/**' + - path: ".github/workflows/**" instructions: | CI pipeline: tests.yml (main CI with changelog, build, lint, sanity, unit jobs), codecoverage.yml, release.yml (Galaxy + Automation Hub publish), check_label.yaml, cla-check.yml. Changes to release.yml or ah_token_refresh.yml affect publishing credentials — review with extra care. Do not remove the `all_green` aggregation job from tests.yml. # ── Unit tests ───────────────────────────────────────────────────── - - path: 'tests/unit/**' + - path: "tests/unit/**" instructions: | Unit tests use pytest + unittest.TestCase via TestVyosModule base class. Key patterns: - All test classes inherit TestVyosModule (from vyos_module.py). - setUp() creates and starts mock patches; tearDown() stops them. - execute_module(failed, changed, commands, sort) is the primary assertion method. - load_fixtures() is overridden per test class to wire mock return values. - Fixture files (.cfg) go in tests/unit/modules/network/vyos/fixtures/. - Use load_fixture(name) to read fixtures — never inline raw config strings. - set_module_args(dict(...)) configures module input before execution. Style: black line-length=100, assertions via self.assertEqual / self.assertIn / execute_module kwargs. pytest-xdist runs tests in parallel (-n 2). # ── Test fixtures ────────────────────────────────────────────────── - - path: 'tests/unit/modules/network/vyos/fixtures/**' + - path: "tests/unit/modules/network/vyos/fixtures/**" instructions: | Raw VyOS CLI output files (.cfg). These are loaded by load_fixture() and cached in memory. Format is VyOS `set ...` configuration syntax or show command output. Fixture filenames follow the pattern: vyos_{module}_config.cfg (base) or vyos_{module}_config_v14.cfg (VyOS 1.4+). New fixtures must be syntactically valid VyOS config. Do not add JSON fixtures unless the test explicitly requires JSON parsing. # ── Collection metadata ──────────────────────────────────────────── - - path: 'galaxy.yml' + - path: "galaxy.yml" instructions: | Collection metadata. namespace=vyos, name=vyos. Version bumps must be coordinated with release process. Dependency on ansible.netcommon>=2.5.1 is required. Do not add unnecessary dependencies. - - path: 'meta/runtime.yml' + - path: "meta/runtime.yml" instructions: | Module redirects and tombstones. Adding a new module requires a redirect entry (short name → FQCN). Tombstoned modules (logging, vyos_logging) must not be un-tombstoned. requires_ansible must stay >=2.15.0 unless explicitly bumping minimum version. finishing_touches: docstrings: enabled: true unit_tests: enabled: true tools: github-checks: enabled: true timeout_ms: 90000 eslint: enabled: false biome: enabled: false actionlint: enabled: true yamllint: enabled: true markdownlint: enabled: true languagetool: enabled: true level: default enabled_only: false gitleaks: enabled: true checkov: enabled: false semgrep: enabled: true ast-grep: essential_rules: true ruff: enabled: false chat: auto_reply: true knowledge_base: opt_out: false learnings: scope: auto issues: scope: auto pull_requests: scope: auto linked_repositories: - repository: "ansible/ansible" instructions: > Core Ansible framework. Reference for module_utils base classes, plugin interfaces (cliconf, terminal, action), module documentation conventions (DOCUMENTATION/EXAMPLES/RETURN YAML blocks), and ansible-test sanity requirements. - repository: "ansible-collections/ansible.netcommon" instructions: > Network common collection. Contains ConfigBase, ResourceModule, FactsBase, NetworkTemplate, and get_resource_connection — the base classes and utilities that vyos.vyos modules directly extend. code_generation: docstrings: language: en-US path_instructions: # ── Module entry points: YAML blocks, not Python docstrings ────── - - path: 'plugins/modules/vyos_*.py' + - path: "plugins/modules/vyos_*.py" instructions: | Do NOT generate Python-style docstrings for these files. Ansible modules use YAML triple-string blocks: DOCUMENTATION, EXAMPLES, and RETURN. If updating these blocks: - DOCUMENTATION must include: module name, author, short_description, description (list of strings), version_added, extends_documentation_fragment (vyos.vyos.vyos), and a full options tree with type, description, and choices/default where applicable. Include a notes section listing supported VyOS versions (1.3.8, 1.4.1, 1.4.2, 1.5 rolling). - EXAMPLES must show at least one task per supported state using FQCN (vyos.vyos.vyos_). - RETURN must document: commands (list, always), before (dict, always), after (dict, when changed), and any module-specific return values. Keep version_added accurate — do not backdate. # ── Argspec: skip auto-generated files ─────────────────────────── - - path: 'plugins/module_utils/network/vyos/argspec/**' + - path: "plugins/module_utils/network/vyos/argspec/**" instructions: | Skip — these files are auto-generated by the Ansible resource module builder and carry a "DO NOT EDIT" header. Do not generate or modify docstrings. # ── Config classes ─────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/config/**' + - path: "plugins/module_utils/network/vyos/config/**" instructions: | Config builder classes extending ConfigBase or ResourceModule. Use reStructuredText-style docstrings (Ansible/Sphinx convention): def method(self, ...): """Short description. :param name: description :type name: type :rtype: type :returns: description """ Document: execute_module(), set_config(), get__facts(), and any method that generates CLI commands. Focus on what state transitions the method handles and what CLI commands it may produce. Do not document trivial __init__ that just calls super(). Some files have auto-generated headers — keep docstrings minimal in those to avoid noise on regeneration. # ── Facts classes ──────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/facts/**' + - path: "plugins/module_utils/network/vyos/facts/**" instructions: | Facts parsers that convert VyOS CLI output to structured dicts. Use rST docstrings. Document: - populate(): what show commands it runs and the dict structure it returns. - render_config() / get_device_data(): the CLI command used and expected output format. - Any regex-heavy parsing method: briefly note what CLI patterns it handles. Skip __init__.py files. # ── RM Templates ───────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/rm_templates/*.py' + - path: "plugins/module_utils/network/vyos/rm_templates/*.py" instructions: | Parser template files with _tmplt_* helper functions and PARSERS lists. Add a module-level docstring describing the resource and VyOS CLI hierarchy covered. For _tmplt_* functions: one-line docstring stating the VyOS command path generated (e.g., "Generate `set protocols bgp neighbor timers ...` commands."). Do not document individual regex PARSERS entries — the patterns are self-describing. Files with _14 suffix target VyOS 1.4+ — note this in the module docstring. # ── Cliconf plugin ────────────────────────────────────────────── - - path: 'plugins/cliconf/vyos.py' + - path: "plugins/cliconf/vyos.py" instructions: | Uses Ansible DOCUMENTATION block for plugin-level docs. For Python methods use rST docstrings. Document: get_device_info(), edit_config(), get_config(), get_diff(), commit(), discard_changes(). Note which methods enter/exit configure mode. # ── Terminal and action plugins ────────────────────────────────── - - path: 'plugins/terminal/vyos.py' + - path: "plugins/terminal/vyos.py" instructions: | Minimal docstrings only. Document on_open_shell() and on_close_shell() explaining what terminal parameters they set. Regex patterns are self-documenting — do not add inline comments to each pattern. - - path: 'plugins/action/vyos.py' + - path: "plugins/action/vyos.py" instructions: | Skip — thin proxy inheriting ActionNetworkModule. No docstrings needed. # ── Utilities ──────────────────────────────────────────────────── - - path: 'plugins/module_utils/network/vyos/utils/*.py' + - path: "plugins/module_utils/network/vyos/utils/*.py" instructions: | Utility functions shared across modules. Use rST docstrings for any function with non-obvious behavior. Document parameters, return types, and side effects. Skip trivial one-liners. # ── Test files ────────────────────────────────────────────────── - - path: 'tests/**' + - path: "tests/**" instructions: | Skip — test files do not need docstrings. Test method names should be self-describing (test_vyos___). unit_tests: path_instructions: # ── Resource module tests ──────────────────────────────────────── - - path: 'plugins/modules/vyos_interfaces.py' + - path: "plugins/modules/vyos_interfaces.py" instructions: &resource_module_tests | Generate tests in tests/unit/modules/network/vyos/test_vyos_.py. Inherit from TestVyosModule (import from vyos_module.py in same directory). Structure: ```python class TestVyosModule(TestVyosModule): module = vyos_ def setUp(self): super().setUp() # Mock get_resource_connection at BOTH levels: self.mock_get_resource_connection_config = patch( "ansible_collections.ansible.netcommon.plugins.module_utils." "network.common.cfg.base.get_resource_connection" ) self.mock_get_resource_connection_facts = patch( "ansible_collections.ansible.netcommon.plugins.module_utils." "network.common.facts.facts.get_resource_connection" ) # Mock the facts get_device_data method: self.mock_execute_show_command = patch( "ansible_collections.vyos.vyos.plugins.module_utils.network." "vyos.facts....get_device_data" ) # Start all patches and store references self.execute_show_command = self.mock_execute_show_command.start() def tearDown(self): super().tearDown() # Stop ALL patches def load_fixtures(self, commands=None, filename=None): def load_from_file(*args, **kwargs): return load_fixture(filename or "vyos__config.cfg") self.execute_show_command.side_effect = load_from_file ``` Required test methods for each resource module: - test_vyos__merged: config change, changed=True, verify commands list - test_vyos__merged_idempotent: no-op, changed=False, commands=[] - test_vyos__replaced: replaced state, changed=True - test_vyos__replaced_idempotent: replaced no-op, changed=False - test_vyos__overridden: full override, changed=True - test_vyos__deleted: deletion, changed=True - test_vyos__gathered: state=gathered, verify result["gathered"] dict - test_vyos__rendered: state=rendered, verify result["rendered"] commands - test_vyos__parsed: state=parsed with running_config, verify output Assertions use self.execute_module(changed=True/False, commands=[...]). Commands lists contain exact VyOS CLI strings: "set interfaces ethernet eth0 ...". Use set_module_args(dict(config=[...], state="")) before execute_module. Create fixture files in tests/unit/modules/network/vyos/fixtures/ named vyos__config.cfg with valid VyOS set-syntax configuration. Use load_fixture() to read fixtures — never inline raw config. - - path: 'plugins/modules/vyos_l3_interfaces.py' + - path: "plugins/modules/vyos_l3_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_lag_interfaces.py' + - path: "plugins/modules/vyos_lag_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_lldp_global.py' + - path: "plugins/modules/vyos_lldp_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_lldp_interfaces.py' + - path: "plugins/modules/vyos_lldp_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_static_routes.py' + - path: "plugins/modules/vyos_static_routes.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_firewall_rules.py' + - path: "plugins/modules/vyos_firewall_rules.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_firewall_global.py' + - path: "plugins/modules/vyos_firewall_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_firewall_interfaces.py' + - path: "plugins/modules/vyos_firewall_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_ospfv2.py' + - path: "plugins/modules/vyos_ospfv2.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_ospfv3.py' + - path: "plugins/modules/vyos_ospfv3.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_ospf_interfaces.py' + - path: "plugins/modules/vyos_ospf_interfaces.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_bgp_global.py' + - path: "plugins/modules/vyos_bgp_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_bgp_address_family.py' + - path: "plugins/modules/vyos_bgp_address_family.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_prefix_lists.py' + - path: "plugins/modules/vyos_prefix_lists.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_route_maps.py' + - path: "plugins/modules/vyos_route_maps.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_snmp_server.py' + - path: "plugins/modules/vyos_snmp_server.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_logging_global.py' + - path: "plugins/modules/vyos_logging_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_ntp_global.py' + - path: "plugins/modules/vyos_ntp_global.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_hostname.py' + - path: "plugins/modules/vyos_hostname.py" instructions: *resource_module_tests - - path: 'plugins/modules/vyos_vrf.py' + - path: "plugins/modules/vyos_vrf.py" instructions: *resource_module_tests # ── Legacy module tests ────────────────────────────────────────── - - path: 'plugins/modules/vyos_command.py' + - path: "plugins/modules/vyos_command.py" instructions: &legacy_module_tests | Generate tests in tests/unit/modules/network/vyos/test_vyos_.py. Inherit from TestVyosModule. Legacy modules mock differently from resource modules: ```python class TestVyosModule(TestVyosModule): module = vyos_ def setUp(self): super().setUp() # Mock run_commands directly on the module: self.mock_run_commands = patch( "ansible_collections.vyos.vyos.plugins.modules." "vyos_.run_commands" ) self.run_commands = self.mock_run_commands.start() # Some also mock get_capabilities or get_config/load_config def tearDown(self): super().tearDown() self.mock_run_commands.stop() def load_fixtures(self, commands=None, filename=None): # Set run_commands return_value or side_effect self.run_commands.return_value = [load_fixture(filename)] ``` Legacy modules (vyos_command, vyos_config, vyos_facts, vyos_banner, vyos_ping, vyos_system, vyos_user, vyos_vlan) do not use resource module states. Test: successful execution, error handling, idempotency where applicable, and specific module features (e.g., vyos_command wait_for/retries, vyos_config src/lines/match). Use execute_module(changed=, commands=) for assertions. - - path: 'plugins/modules/vyos_config.py' + - path: "plugins/modules/vyos_config.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_facts.py' + - path: "plugins/modules/vyos_facts.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_banner.py' + - path: "plugins/modules/vyos_banner.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_ping.py' + - path: "plugins/modules/vyos_ping.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_system.py' + - path: "plugins/modules/vyos_system.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_user.py' + - path: "plugins/modules/vyos_user.py" instructions: *legacy_module_tests - - path: 'plugins/modules/vyos_vlan.py' + - path: "plugins/modules/vyos_vlan.py" instructions: *legacy_module_tests # ── Test infrastructure — do not generate tests for these ──────── - - path: 'tests/unit/modules/utils.py' + - path: "tests/unit/modules/utils.py" instructions: | Skip — test infrastructure (ModuleTestCase base, set_module_args, exception classes). Do not generate tests for test utilities. - - path: 'tests/unit/modules/conftest.py' + - path: "tests/unit/modules/conftest.py" instructions: | Skip — pytest fixtures (patch_ansible_module). Do not generate tests. - - path: 'tests/unit/modules/network/vyos/vyos_module.py' + - path: "tests/unit/modules/network/vyos/vyos_module.py" instructions: | Skip — TestVyosModule base class with execute_module(), load_fixture(), and mock setup. Do not generate tests for the test base class. - - path: 'tests/unit/modules/network/vyos/fixtures/**' + - path: "tests/unit/modules/network/vyos/fixtures/**" instructions: | Skip — raw VyOS CLI output fixtures. Not code, not testable. # ── Non-module plugin code ─────────────────────────────────────── - - path: 'plugins/module_utils/**' + - path: "plugins/module_utils/**" instructions: | Module utility code (argspec, config, facts, rm_templates, utils). These are tested indirectly through module-level tests — the config classes are exercised when test_vyos_.py calls execute_module(). Do not generate separate unit tests for module_utils classes unless a utility function in plugins/module_utils/network/vyos/utils/ has complex standalone logic worth testing in isolation. - - path: 'plugins/cliconf/vyos.py' + - path: "plugins/cliconf/vyos.py" instructions: | Skip — cliconf plugin is tested via integration tests and indirectly through module tests. Unit testing requires complex CliconfBase mocking that provides little value over integration coverage. - - path: 'plugins/terminal/vyos.py' + - path: "plugins/terminal/vyos.py" instructions: | Skip — terminal plugin regex patterns are validated through integration tests against actual VyOS devices. - - path: 'plugins/action/vyos.py' + - path: "plugins/action/vyos.py" instructions: | Skip — thin action proxy. Tested indirectly via module tests. diff --git a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py index b6f10106..5bd564c2 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py +++ b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py @@ -1,521 +1,515 @@ # # -*- coding: utf-8 -*- # Copyright 2019 Red Hat # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ The vyos firewall_global fact class It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy from re import M, findall, search from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.firewall_global.firewall_global import ( Firewall_globalArgs, ) class Firewall_globalFacts(object): """The vyos firewall_global fact class""" def __init__(self, module, subspec="config", options="options"): self._module = module self.argument_spec = Firewall_globalArgs.argument_spec spec = deepcopy(self.argument_spec) if subspec: if options: facts_argument_spec = spec[subspec][options] else: facts_argument_spec = spec[subspec] else: facts_argument_spec = spec self.generated_spec = utils.generate_dict(facts_argument_spec) def get_device_data(self, connection): return connection.get_config() def populate_facts(self, connection, ansible_facts, data=None): """Populate the facts for firewall_global :param connection: the device connection :param ansible_facts: Facts dictionary :param data: previously collected conf :rtype: dictionary :returns: facts """ if not data: # typically data is populated from the current device configuration # data = connection.get('show running-config | section ^interface') # using mock data instead data = self.get_device_data(connection) objs = {} firewalls = findall(r"^set firewall .*$", data, M) if firewalls: objs = self.render_config(firewalls) facts = {} params = utils.validate_config(self.argument_spec, {"config": objs}) facts["firewall_global"] = utils.remove_empties(params["config"]) ansible_facts["ansible_network_resources"].update(facts) return ansible_facts def render_config(self, conf): """ Render config as dictionary structure and delete keys from spec for null values :param spec: The facts tree, generated from the argspec :param conf: The configuration :rtype: dictionary :returns: The generated config """ - # conf = "\n".join( - # filter( - # lambda x: ("firewall ipv6-name" and "firewall name" not in x), - # conf, - # ), - # ) conf = "\n".join( filter( lambda x: not ( x.startswith("set firewall ipv6 name") or x.startswith("set firewall ipv6 name") ), conf, ), ) a_lst = [ "config_trap", "validation", "log_martians", "syn_cookies", "twa_hazards_protection", ] firewall = self.parse_attr(conf, a_lst) f_sub = { "ping": self.parse_ping(conf), "group": self.parse_group(conf), "route_redirects": self.route_redirects(conf), "state_policy": self.parse_state_policy(conf), "zone": self.parse_zone(conf), } firewall.update(f_sub) return firewall def route_redirects(self, conf): """ This function forms the regex to fetch the afi and invoke functions to fetch route redirects and source routes :param conf: configuration data. :return: generated rule list configuration. """ rr_lst = [] v6_attr = findall( r"^set firewall (?:global-options )?(?:ipv6-src-route|ipv6-receive-redirects) (\S+)", conf, M, ) if v6_attr: obj = self.parse_rr_attrib(conf, "ipv6") if obj: rr_lst.append(obj) v4_attr = findall( r"^set firewall (?:global-options )?(?:ip-src-route|receive-redirects|send-redirects) (\S+)", conf, M, ) if v4_attr: obj = self.parse_rr_attrib(conf, "ipv4") if obj: rr_lst.append(obj) return rr_lst def parse_rr_attrib(self, conf, attrib=None): """ This function fetches the 'ip_src_route' invoke function to parse icmp redirects. :param conf: configuration to be parsed. :param attrib: 'ipv4/ipv6'. :return: generated config dictionary. """ cfg_dict = self.parse_attr(conf, ["ip_src_route"], type=attrib) cfg_dict["icmp_redirects"] = self.parse_icmp_redirects(conf, attrib) cfg_dict["afi"] = attrib return cfg_dict def parse_icmp_redirects(self, conf, attrib=None): """ This function triggers the parsing of 'icmp_redirects' attributes. :param conf: configuration to be parsed. :param attrib: 'ipv4/ipv6'. :return: generated config dictionary. """ a_lst = ["send", "receive"] cfg_dict = self.parse_attr(conf, a_lst, type=attrib) return cfg_dict def parse_ping(self, conf): """ This function triggers the parsing of 'ping' attributes. :param conf: configuration to be parsed. :return: generated config dictionary. """ a_lst = ["all", "broadcast"] cfg_dict = self.parse_attr(conf, a_lst) return cfg_dict def parse_state_policy(self, conf): """ This function fetched the connecton type and invoke function to parse other state-policy attributes. :param conf: configuration data. :return: generated rule list configuration. """ sp_lst = [] policies = findall(r"^set firewall (?:global-options )?state-policy (\S+)", conf, M) policies = list(set(policies)) # remove redundancies if policies: rules_lst = [] for sp in set(policies): sp_regex = r"^set firewall (?:global-options )?state-policy %s .+$" % sp cfg = "\n".join(findall(sp_regex, conf, M)) obj = self.parse_policies(cfg, sp) obj["connection_type"] = sp if obj: rules_lst.append(obj) sp_lst = sorted(rules_lst, key=lambda i: i["connection_type"]) return sp_lst def parse_policies(self, conf, attrib=None): """ This function triggers the parsing of policy attributes action and log. :param conf: configuration :param attrib: connection type. :return: generated rule configuration dictionary. """ a_lst = ["action", "log", "log_level"] cfg_dict = self.parse_attr(conf, a_lst, match=attrib) return cfg_dict def parse_group(self, conf): """ This function triggers the parsing of 'group' attributes. :param conf: configuration. :return: generated config dictionary. """ cfg_dict = {} cfg_dict["port_group"] = self.parse_group_lst(conf, "port-group", False) cfg_dict["address_group"] = self.parse_group_lst( conf, "address-group", ) + self.parse_group_lst(conf, "ipv6-address-group") cfg_dict["network_group"] = self.parse_group_lst( conf, "network-group", ) + self.parse_group_lst(conf, "ipv6-network-group") return cfg_dict def parse_group_lst(self, conf, type, include_afi=True): """ This function fetches the name of group and invoke function to parse group attributes'. :param conf: configuration data. :param type: type of group. :param include_afi: if the afi should be included in the parsed object :return: generated group list configuration. """ g_lst = [] groups = findall(r"^set firewall group " + type + " (\\S+)", conf, M) if groups: rules_lst = [] for gr in set(groups): gr_regex = r"^set firewall group " + type + " %s .+$" % gr cfg = "\n".join(findall(gr_regex, conf, M)) if "ipv6" in type: # fmt: off obj = self.parse_groups(cfg, type[len("ipv6-"):], gr) # fmt: on if include_afi: obj["afi"] = "ipv6" else: obj = self.parse_groups(cfg, type, gr) if include_afi: obj["afi"] = "ipv4" obj["name"] = gr.strip("'") if obj: rules_lst.append(obj) g_lst = sorted(rules_lst, key=lambda i: i["name"]) return g_lst def parse_groups(self, conf, type, name): """ This function fetches the description and invoke the parsing of group members. :param conf: configuration. :param type: type of group. :param name: name of group. :return: generated configuration dictionary. """ a_lst = ["name", "description"] group = self.parse_attr(conf, a_lst) key = self.get_key(type) r_sub = {key[0]: self.parse_address_port_lst(conf, name, key[1])} group.update(r_sub) return group def parse_address_port_lst(self, conf, name, key): """ This function forms the regex to fetch the group members attributes. :param conf: configuration data. :param name: name of group. :param key: key value. :return: generated member list configuration. """ l_lst = [] attribs = findall(r"^.*" + name + " " + key + " (\\S+)", conf, M) if attribs: for attr in attribs: if key == "port": l_lst.append({"port": attr.strip("'")}) else: l_lst.append({"address": attr.strip("'")}) return l_lst def parse_attr(self, conf, attr_list, match=None, type=None): """ This function peforms the following: - Form the regex to fetch the required attribute config. - Type cast the output in desired format. :param conf: configuration. :param attr_list: list of attributes. :param match: parent node/attribute name. :return: generated config dictionary. """ config = {} for attrib in attr_list: regex = self.map_regex(attrib, type) if match: regex = match + " " + regex if conf: if self.is_bool(attrib): # fancy regex to make sure we don't get a substring out = search(r"^.*" + regex + r"( 'disable')?(?=\s|$)", conf, M) if out: if out.group(1): config[attrib] = False else: config[attrib] = True else: out = search(r"^.*" + regex + r" (.+)", conf, M) if out: val = out.group(1).strip("'") if self.is_num(attrib): val = int(val) config[attrib] = val return config def get_key(self, type): """ This function map the group type to member type :param type: :return: """ key = () if type == "port-group": key = ("members", "port") elif type == "address-group": key = ("members", "address") elif type == "network-group": key = ("members", "network") return key def map_regex(self, attrib, type=None): """ - This function construct the regex string. - replace the underscore with hyphen. :param attrib: attribute :return: regex string """ regex = attrib.replace("_", "-") if attrib == "all": regex = "all-ping" elif attrib == "disabled": regex = "disable" elif attrib == "broadcast": regex = "broadcast-ping" elif attrib == "send": if type == "ipv6": regex = "ipv6-send-redirects" else: regex = "send-redirects" elif attrib == "ip_src_route": if type == "ipv6": regex = "ipv6-src-route" elif attrib == "receive": if type == "ipv6": regex = "ipv6-receive-redirects" else: regex = "receive-redirects" return regex def is_num(self, attrib): """ This function looks for the attribute in predefined integer type set. :param attrib: attribute. :return: True/false. """ num_set = ("time", "code", "type", "count", "burst", "number") return True if attrib in num_set else False def get_src_route(self, attrib): """ This function looks for the attribute in predefined integer type set. :param attrib: attribute. :return: True/false. """ return "ipv6_src_route" if attrib == "ipv6" else "ip_src_route" def is_bool(self, attrib): """ This function looks for the attribute in predefined bool type set. :param attrib: attribute. :return: True/False """ bool_set = ( "all", "log", "send", "receive", "broadcast", "config_trap", "log_martians", "syn_cookies", "ip_src_route", "twa_hazards_protection", ) return True if attrib in bool_set else False def parse_zone(self, conf): """ This function triggers the parsing of 'zone' attributes. :param conf: configuration. :return: generated config dictionary. """ cfg_dict = {} KEY_MAP = { "interface": "interfaces", "intra-zone-filtering": "intra-zone-filtering", "from": "sources", } LIST_ATTRS = { "interfaces", "intra_zone_filtering", "sources", } for line in conf.splitlines(): m = search( r"^set firewall zone (?P\S+)\s+(?P[a-z-]+)(?:\s+(?P'[^']+'|[^\n]+))?$", line, ) if not m: continue zone_name = m.group("zone") raw_attr = m.group("attr").replace("-", "_") value = m.group("value") if value is None: value = True else: value = value.strip("'") zone = cfg_dict.setdefault(zone_name, {"name": zone_name}) attr = KEY_MAP.get(raw_attr, raw_attr) if attr in LIST_ATTRS: if attr == "intra_zone_filtering": izf = zone.setdefault(attr, {}) izf_attr = self._parse_izf(value) for k, v in izf_attr.items(): if isinstance(v, dict): izf.setdefault(k, {}).update(v) else: izf[k] = v elif attr == "sources": self._parse_sources(zone, value) else: zone.setdefault(attr, []).append(value) else: zone[attr] = value # self._module.fail_json(msg={"cfg_dict": cfg_dict}) return list(cfg_dict.values()) def _parse_izf(self, value): tokens = value.replace("'", "").split() result = {} key = tokens[0].replace("-", "_") if len(tokens) == 2: result[key] = tokens[1] elif len(tokens) >= 3: subkey = tokens[1].replace("-", "_") result[key] = {subkey: tokens[2]} return result def _parse_sources(self, zone, value): tokens = value.split() if len(tokens) < 1: return src_zone = tokens[0] sources = zone.setdefault("sources", []) entry = None for s in sources: if s.get("zone") == src_zone: entry = s break if entry is None: entry = {"zone": src_zone} sources.append(entry) if len(tokens) == 1: return if tokens[1] == "firewall" and len(tokens) >= 4: key = tokens[2].replace("-", "_") val = tokens[3].strip("'") firewall = entry.setdefault("firewall", {}) firewall[key] = val diff --git a/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_4.cfg b/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_4.cfg index 13237bd8..9c57217f 100644 --- a/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_4.cfg +++ b/tests/integration/targets/vyos_firewall_global/tests/cli/_parsed_config_1_4.cfg @@ -1,22 +1,21 @@ set firewall global-options all-ping 'enable' set firewall global-options broadcast-ping 'enable' set firewall group address-group MGMT-HOSTS address '192.0.1.1' set firewall group address-group MGMT-HOSTS address '192.0.1.3' set firewall group address-group MGMT-HOSTS address '192.0.1.5' set firewall group address-group MGMT-HOSTS description 'This group has the Management hosts address list' set firewall group network-group MGMT description 'This group has the Management network addresses' set firewall group network-group MGMT network '192.0.1.0/24' set firewall global-options ip-src-route 'enable' set firewall global-options log-martians 'enable' set firewall global-options receive-redirects 'disable' set firewall global-options send-redirects 'enable' set firewall global-options source-validation 'strict' set firewall global-options state-policy established action 'accept' set firewall global-options state-policy established log 'enable' -# set firewall global-options state-policy invalid acti. .on 'reject' set firewall global-options syn-cookies 'enable' set firewall global-options twa-hazards-protection 'enable' set firewall global-options twa-hazards-protection 'enable' set firewall zone ZONE-TEST interface 'eth0.1234' set firewall zone ZONE-TEST description 'zone-test test description' set firewall zone ZONE-TEST default-action 'drop'