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/AGENTS.md b/AGENTS.md index 436b6785..6a749230 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,39 +1,46 @@ # AGENTS.md ## Project purpose + The official Ansible Collection for managing VyOS network appliances (`vyos.vyos` namespace). Provides modules, plugins, action handlers, terminal plugins, and resource modules for BGP, OSPF, firewall, interfaces, NTP, etc. ## Tech stack + - Ansible Collection (Galaxy). Python control-plane code under `plugins/`. - `galaxy.yml` declares `namespace: vyos`, `name: vyos`, `version: 6.0.0`, dep `ansible.netcommon >= 2.5.1`, license_file `LICENSE` (GPL-3.0). - Test stack: `pytest` + `tox-ansible.ini`; lint via flake8, isort, black (line-length 100), pre-commit, ansible-lint. - Runtime deps: `paramiko`, `scp` (`requirements.txt`); `bindep.txt` for system deps. ## Build / test / run + - Build: `ansible-galaxy collection build` produces a `vyos-vyos-.tar.gz`. - Install local dev: `ansible-galaxy collection install . --force`. - Test (unit): `ansible-test units` (matches `unit-galaxy` CI job; requires collection installed under `~/.ansible/collections/`). Fast local alternative: `source .venv/bin/activate && PYTHONPATH=".collections" python -m pytest tests/unit` (matches `unit-source` CI path; config in `pyproject.toml`). CI (`.github/workflows/tests.yml`) runs the changelog / build-import / ansible-lint / sanity / unit-galaxy / unit-source jobs; integration tests live under `tests/integration/` but are not yet wired into CI. Per `README.md`, the collection targets VyOS 1.3.8 / 1.4.1 / 1.5-rolling (no version matrix in the workflow itself). ## Repository layout + - `plugins/{action,cliconf,doc_fragments,filter,inventory,module_utils,modules,terminal}/` — collection content. - `tests/` — sanity, unit, integration directories. CI (`.github/workflows/tests.yml`) runs sanity + unit-galaxy + unit-source (plus changelog / build-import / ansible-lint); integration is not yet wired into CI. - `docs/` — generated module docs. - `meta/`, `changelogs/`, `CHANGELOG.rst` — Galaxy + release metadata. - `pyproject.toml` (black/pytest config), `.flake8`, `.isort.cfg`, `.ansible-lint`, `.pre-commit-config.yaml`. - `.github/workflows/` — `tests.yml`, `release.yml`, `codecoverage.yml`, `cla-check.yml`, `ah_token_refresh.yml`, `check_label.yaml`. ## Cross-repo context + - Consumed by Ansible users running playbooks against VyOS routers built by `vyos/vyos-build`. - The `vyos.vyos` collection talks to VyOS via `network_cli` connections; supports the same train branches (`current`, `circinus`, `sagitta`, `equuleus`). ## Conventions + - Commit headline: `T12345: description` (Phorge ID at https://vyos.dev mandatory). No workflow enforces PR title format in this repo. - Every PR must include exactly one changelog fragment under `changelogs/fragments/`; use `doc_changes` for documentation-only updates, or `trivial` for tooling / housekeeping changes. - Default branch `main` (not `current` — this repo predates the rename convention). - Issues tracked at https://vyos.dev (see `galaxy.yml`). - Codecov + CodeRabbit configured (`codecov.yml`, `.coderabbit.yaml`). ## Notes for future contributors + - Galaxy versioning is independent of VyOS train versioning — bump in `galaxy.yml` per release. - Tested matrix is in README; expand only after smoketesting against real images. - `PR408_README.md` plus `pr408-diagram.png` document a non-trivial historical refactor; read before touching resource-module structure. diff --git a/plugins/module_utils/network/vyos/facts/ha/ha.py b/plugins/module_utils/network/vyos/facts/ha/ha.py index 0cd71592..7cc8bb6f 100644 --- a/plugins/module_utils/network/vyos/facts/ha/ha.py +++ b/plugins/module_utils/network/vyos/facts/ha/ha.py @@ -1,164 +1,164 @@ # -*- coding: utf-8 -*- # Copyright 2021 Red Hat # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type """ The vyos_ha 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. """ import re from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ha.ha import ( HaArgs, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ha import ( HaTemplate, ) class HaFacts(object): """The vyos_ha facts class""" def __init__(self, module, subspec="config", options="options"): self._module = module self.argument_spec = HaArgs.argument_spec def get_config(self, connection): return connection.get('show configuration commands | match "set high-availability"') def get_config_set(self, data, connection): - """To classify the configurations beased on high availability sections""" + """To classify the configurations based on high availability sections""" config_dict = {} for config_line in data.splitlines(): vrrp_grp = re.search(r"set high-availability vrrp group (\S+).*", config_line) vrrp_gp = re.search( r"set high-availability vrrp global-parameters (\S+).*", config_line, ) vrrp_sg = re.search(r"set high-availability vrrp sync-group (\S+).*", config_line) vrrp_vsrv = re.search(r"set high-availability virtual-server (\S+).*", config_line) vrrp_disable = re.search(r"set high-availability disable", config_line) vrrp_snmp = re.search(r"set high-availability vrrp snmp", config_line) if vrrp_disable: config_dict["disable"] = [config_dict.get("disable", "") + config_line] if vrrp_snmp: config_dict.setdefault("vrrp", []).append(config_line) if vrrp_gp: config_dict.setdefault("global_parameters", []).append(config_line) if vrrp_vsrv: config_dict.setdefault(vrrp_vsrv.group(1), []).append(config_line) if vrrp_sg: config_dict.setdefault(vrrp_sg.group(1), []).append(config_line) if vrrp_grp: config_dict.setdefault(vrrp_grp.group(1), []).append(config_line) return list(config_dict.values()) def deep_merge(self, dest, src): for key, value in src.items(): if key in dest and isinstance(dest[key], dict) and isinstance(value, dict): self.deep_merge(dest[key], value) else: dest[key] = value return dest def populate_facts(self, connection, ansible_facts, data=None): """Populate the facts for vrrp network resource :param connection: the device connection :param ansible_facts: Facts dictionary :param data: previously collected conf :rtype: dictionary :returns: facts """ facts = {} objs = {} if not data: data = self.get_config(connection) resources = self.get_config_set(data, connection) vrrp_facts = {"disable": False, "virtual_servers": {}, "vrrp": {}} for resource in resources: vrrp_parser = HaTemplate( lines=resource, module=self._module, ) objs = vrrp_parser.parse() if "disable" in objs: vrrp_facts["disable"] = objs["disable"] for section in ("virtual_servers", "vrrp"): if section in objs: for name, data in objs[section].items(): if not isinstance(data, dict): vrrp_facts[section][name] = data continue existing = vrrp_facts[section].get(name, {}) vrrp_facts[section][name] = self.deep_merge(existing, data) - ansible_facts["ansible_network_resources"].pop("vrrp", None) + ansible_facts["ansible_network_resources"].pop("ha", None) vrrp_facts = self.normalize_config(vrrp_facts) validate_parser = HaTemplate(lines=[], module=self._module) params = utils.remove_empties( validate_parser.validate_config( self.argument_spec, {"config": vrrp_facts}, redact=True, ), ) facts["ha"] = self.normalize_config(params.get("config", [])) ansible_facts["ansible_network_resources"].update(facts) return ansible_facts def normalize_config(self, config): if not config: return config # Normalize virtual_servers if isinstance(config.get("virtual_servers"), dict): config["virtual_servers"] = list(config["virtual_servers"].values()) # Normalize vrrp vrrp = config.get("vrrp", {}) if isinstance(vrrp.get("groups"), dict): vrrp["groups"] = list(vrrp["groups"].values()) if isinstance(vrrp.get("sync_groups"), dict): vrrp["sync_groups"] = list(vrrp["sync_groups"].values()) # Normalize real_server inside each virtual_server for vs in config.get("virtual_servers", []): if isinstance(vs.get("real_server"), dict): vs["real_server"] = list(vs["real_server"].values()) vrrp = config.get("vrrp", {}) for group in vrrp.get("groups", []): if isinstance(group.get("address"), list): group["address"] = sorted(group["address"]) if isinstance(group.get("excluded_address"), list): group["excluded_address"] = sorted(group["excluded_address"]) if isinstance(group.get("track", {}).get("interface"), list): group["track"]["interface"] = sorted(group["track"]["interface"]) for sg in vrrp.get("sync_groups", []): if isinstance(sg.get("member"), list): sg["member"] = sorted(sg["member"]) return config diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py index f85eabf1..5dbc9db6 100644 --- a/plugins/module_utils/network/vyos/utils/utils.py +++ b/plugins/module_utils/network/vyos/utils/utils.py @@ -1,345 +1,323 @@ # -*- 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) # utils from __future__ import absolute_import, division, print_function __metaclass__ = type from ansible.module_utils.basic import missing_required_lib try: import ipaddress HAS_IPADDRESS = True except ImportError: HAS_IPADDRESS = False def search_obj_in_list(name, lst, key="name"): if lst: for item in lst: if item[key] == name: return item return None def get_interface_type(interface): """Gets the type of interface""" if interface.startswith("eth"): return "ethernet" elif interface.startswith("bond"): return "bonding" elif interface.startswith("vti"): return "vti" elif interface.startswith("lo"): return "loopback" elif interface.startswith("vtun"): return "openvpn" elif interface.startswith("wg"): return "wireguard" elif interface.startswith("tun"): return "tunnel" elif interface.startswith("br"): return "bridge" elif interface.startswith("dum"): return "dummy" def get_interface_with_vif(interface): """Gets virtual interface if any or return as is""" vlan = None interface_real = interface if "." in interface: interface_real, vlan = interface.split(".") if vlan is not None: interface_real = interface_real + " vif " + vlan return interface_real def dict_delete(base, comparable): """ This function generates a dict containing key, value pairs for keys that are present in the `base` dict but not present in the `comparable` dict. :param base: dict object to base the diff on :param comparable: dict object to compare against base :returns: new dict object with key, value pairs that needs to be deleted. """ to_delete = dict() for key in base: if isinstance(base[key], dict): sub_diff = dict_delete(base[key], comparable.get(key, {})) if sub_diff: to_delete[key] = sub_diff else: if key not in comparable: to_delete[key] = base[key] return to_delete def diff_list_of_dicts(want, have): diff = [] set_w = set(tuple(d.items()) for d in want) set_h = set(tuple(d.items()) for d in have) difference = set_w.difference(set_h) for element in difference: diff.append(dict((x, y) for x, y in element)) return diff def get_lst_diff_for_dicts(want, have, lst): """ This function generates a list containing values that are only in want and not in list in have dict :param want: dict object to want :param have: dict object to have :param lst: list the diff on :return: new list object with values which are only in want. """ if not have: diff = want.get(lst) or [] else: want_elements = want.get(lst) or {} have_elements = have.get(lst) or {} diff = list_diff_want_only(want_elements, have_elements) return diff def get_lst_same_for_dicts(want, have, lst): """ This function generates a list containing values that are common for list in want and list in have dict :param want: dict object to want :param have: dict object to have :param lst: list the comparison on :return: new list object with values which are common in want and have. """ diff = None if want and have: want_list = want.get(lst) or {} have_list = have.get(lst) or {} diff = [i for i in want_list and have_list if i in have_list and i in want_list] return diff def list_diff_have_only(want_list, have_list): """ This function generated the list containing values that are only in have list. :param want_list: :param have_list: :return: new list with values which are only in have list """ if have_list and not want_list: diff = have_list elif not have_list: diff = None else: diff = [i for i in have_list + want_list if i in have_list and i not in want_list] return diff def list_diff_want_only(want_list, have_list): """ This function generated the list containing values that are only in want list. :param want_list: :param have_list: :return: new list with values which are only in want list """ if have_list and not want_list: diff = None elif not have_list: diff = want_list else: diff = [i for i in have_list + want_list if i in want_list and i not in have_list] return diff def search_dict_tv_in_list(d_val1, d_val2, lst, key1, key2): """ This function return the dict object if it exist in list. :param d_val1: :param d_val2: :param lst: :param key1: :param key2: :return: """ obj = next( (item for item in lst if item[key1] == d_val1 and item[key2] == d_val2), None, ) if obj: return obj else: return None def key_value_in_dict(have_key, have_value, want_dict): """ This function checks whether the key and values exist in dict :param have_key: :param have_value: :param want_dict: :return: """ for key, value in want_dict.items(): if key == have_key and value == have_value: return True return False def is_dict_element_present(dict, key): """ This function checks whether the key is present in dict. :param dict: :param key: :return: """ for item in dict: if item == key: return True return False def get_ip_address_version(address): """ This function returns the version of IP address :param address: IP address :return: """ if not HAS_IPADDRESS: raise Exception(missing_required_lib("ipaddress")) try: address = unicode(address) except NameError: address = str(address) version = ipaddress.ip_address(address.split("/")[0]).version return version def get_route_type(address): """ This function returns the route type based on IP address :param address: :return: """ version = get_ip_address_version(address) if version == 6: return "route6" elif version == 4: return "route" def _bool_to_str(val): """ This function converts the bool value into string. :param val: bool value. :return: enable/disable. """ return "enable" if str(val) == "True" else "disable" if str(val) == "False" else val def _is_w_same(w, h, key): """ This function checks whether the key value is same in desired and target config dictionary. :param w: base config. :param h: target config. :param key:attribute name. :return: True/False. """ return True if h and key in h and h[key] == w[key] else False def _in_target(h, key): """ This functi checks whether the target exist and key present in target config. :param h: target config. :param key: attribute name. :return: True/False. """ return True if h and key in h else False def in_target_not_none(h, key): """ This function checks whether the target exist,key present in target config, and the value is not None. :param h: target config. :param key: attribute name. :return: True/False. """ return True if h and key in h and h[key] is not None else False -# def combine(a, b, recursive=False, list_merge="replace"): -# """ -# Merge two dictionaries (shallow or deep). -# :param a: dict -# :param b: dict -# :param recursive: bool, deep merge -# :param list_merge: str, only 'replace' is supported (default Ansible behavior) -# """ -# if not isinstance(a, dict) or not isinstance(b, dict): -# raise ValueError("combine expects two dictionaries") - -# result = a.copy() - -# for k, v in b.items(): -# if recursive and k in result and isinstance(result[k], dict) and isinstance(v, dict): -# result[k] = combine(result[k], v, recursive=True, list_merge=list_merge) -# else: -# result[k] = v - -# return result - - def combine(a, b, recursive=False, list_merge="replace"): if not isinstance(a, dict) or not isinstance(b, dict): raise ValueError("combine expects two dictionaries") result = a.copy() for k, v in b.items(): if k in result: # dict merge if recursive and isinstance(result[k], dict) and isinstance(v, dict): result[k] = combine(result[k], v, recursive=True, list_merge=list_merge) # list merge elif isinstance(result[k], list) and isinstance(v, list): if list_merge == "replace": result[k] = v elif list_merge == "append": result[k] = result[k] + v elif list_merge == "prepend": result[k] = v + result[k] elif list_merge == "append_rp": result[k] = list(dict.fromkeys(result[k] + v)) elif list_merge == "prepend_rp": result[k] = list(dict.fromkeys(v + result[k])) else: raise ValueError(f"Unsupported list_merge mode: {list_merge}") # everything else else: result[k] = v else: result[k] = v return result diff --git a/tests/unit/modules/network/vyos/test_vyos_ha.py b/tests/unit/modules/network/vyos/test_vyos_ha.py index 5b11f644..f5e5890b 100644 --- a/tests/unit/modules/network/vyos/test_vyos_ha.py +++ b/tests/unit/modules/network/vyos/test_vyos_ha.py @@ -1,1394 +1,1394 @@ # (c) 2021 Red Hat Inc. # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # Make coding more python3-ish from __future__ import absolute_import, division, print_function __metaclass__ = type from unittest.mock import patch from ansible_collections.vyos.vyos.plugins.modules import vyos_ha from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args from .vyos_module import TestVyosModule, load_fixture class TestVyosHaModule(TestVyosModule): module = vyos_ha def setUp(self): super(TestVyosHaModule, self).setUp() self.mock_get_resource_connection_config = patch( "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base.get_resource_connection", ) self.get_resource_connection_config = self.mock_get_resource_connection_config.start() self.mock_get_resource_connection_facts = patch( "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection", ) self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start() self.mock_execute_show_command = patch( "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ha.ha.HaFacts.get_config", ) self.execute_show_command = self.mock_execute_show_command.start() self.mock_get_os_version = patch( "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ha.ha.get_os_version", ) self.get_os_version = self.mock_get_os_version.start() self.get_os_version.return_value = "1.5" self.maxDiff = None def tearDown(self): super(TestVyosHaModule, self).tearDown() self.mock_get_resource_connection_config.stop() self.mock_get_resource_connection_facts.stop() self.mock_execute_show_command.stop() self.mock_get_os_version.stop() def load_fixtures(self, commands=None, filename=None): if filename is None: filename = "vyos_ha_config.cfg" def load_from_file(*args, **kwargs): output = load_fixture(filename) return output self.execute_show_command.side_effect = load_from_file def test_vrrp_merged_idempotent(self): set_module_args( dict( config=dict( disable=True, virtual_servers=[ dict( address="10.10.10.1", algorithm="round-robin", delay_loop=60, forward_method="direct", fwmark=10, name="s1", persistence_timeout="30", protocol="tcp", real_server=[ dict( address="10.10.50.1", port=443, health_check_script="/var/tmp/script.sh", ), ], ), dict( address="10.10.10.2", name="s2", port=81, real_server=[ dict( address="real1", port=8081, connection_timeout=5, ), dict( address="real2", port=8080, ), ], ), ], vrrp=dict( global_parameters=dict( garp=dict( master_refresh=100, ), version="3", ), groups=[ dict( authentication=dict( password="testpass", type="plaintext-password", ), address=[ "1.1.1.1", "3.3.3.3", "5.5.5.5 interface eth2", ], advertise_interval=10, description="Group_1", disable=True, excluded_address=[ "192.168.1.8", "192.168.1.7 interface eth3", ], garp=dict( interval=20, master_delay=5, master_refresh_repeat=100, master_repeat=3, ), health_check=dict( failure_count=3, interval=10, ping="192.168.1.5", script="script.sh", ), hello_source_address="192.168.1.2", interface="eth2", name="g1", no_preempt=True, peer_address="192.168.1.3", priority=100, rfc3768_compatibility=True, track=dict( exclude_vrrp_interface=True, interface=["eth1"], ), transition_script=dict( backup="/var/tmp/script.sh", master="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), vrid=20, ), ], snmp="enabled", sync_groups=[ dict( health_check=dict( failure_count=3, interval=10, ), member=["g1"], name="sg1", transition_script=dict( backup="/var/tmp/script.sh", master="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), ), ], ), ), state="merged", ), ) self.execute_module(changed=False, commands=[]) def test_vrrp_merged(self): set_module_args( dict( config=dict( disable=True, virtual_servers=[ dict( name="s1", address="10.10.10.5", algorithm="round-robin", real_server=[ dict( address="10.10.50.2", port=8443, ), ], ), dict( name="s2", address="10.10.10.2", persistence_timeout=30, port=81, protocol="tcp", ), dict( name="s3", address="10.10.10.3", port=88, protocol="udp", ), ], vrrp=dict( snmp="disabled", global_parameters=dict( startup_delay=35, garp=dict( master_repeat=6, ), ), groups=[ dict( name="g1", peer_address="192.168.1.3", priority=100, disable=False, no_preempt=False, ), dict( name="g2", address=[ "192.168.3.3", "192.168.4.4 interface eth3", ], ), ], sync_groups=[ dict( name="sg1", health_check=dict( failure_count=5, ), ), ], ), ), state="merged", ), ) commands = [ "delete high-availability vrrp group g1 no-preempt", "delete high-availability vrrp group g1 disable", "delete high-availability vrrp group g1 rfc3768-compatibility", "set high-availability vrrp group g2 address 192.168.3.3", "set high-availability vrrp group g2 address 192.168.4.4 interface eth3", "delete high-availability vrrp snmp", "set high-availability virtual-server s1 address 10.10.10.5", "set high-availability virtual-server s1 real-server 10.10.50.2 port 8443", "set high-availability virtual-server s2 persistence-timeout 30", "set high-availability virtual-server s2 protocol tcp", "set high-availability virtual-server s3 address 10.10.10.3", "set high-availability virtual-server s3 port 88", "set high-availability virtual-server s3 protocol udp", "set high-availability vrrp global-parameters garp master-repeat 6", "set high-availability vrrp global-parameters startup-delay 35", "set high-availability vrrp sync-group sg1 health-check failure-count 5", ] self.execute_module(changed=True, commands=commands) def test_vrrp_replaced(self): set_module_args( dict( config=dict( disable=False, virtual_servers=[ dict( name="s1", address="10.10.10.5", algorithm="round-robin", real_server=[ dict( address="10.10.50.2", port=8443, ), ], ), dict( name="s2", address="10.10.10.2", persistence_timeout=30, port=81, protocol="tcp", ), dict( name="s3", address="10.10.10.3", port=88, protocol="udp", ), ], vrrp=dict( snmp="disabled", global_parameters=dict( startup_delay=32, garp=dict( master_repeat=6, ), ), groups=[ dict( name="g1", peer_address="192.168.1.3", priority=100, disable=False, no_preempt=False, vrid=20, ), dict( name="g2", address=[ "192.168.3.3", "192.168.4.4 interface eth3", ], ), ], sync_groups=[ dict( name="sg1", health_check=dict( failure_count=5, ), ), ], ), ), state="replaced", ), ) commands = [ "delete high-availability vrrp global-parameters garp master-repeat 5", "delete high-availability vrrp global-parameters startup-delay 30", "delete high-availability vrrp group g1 disable", "delete high-availability vrrp group g1 no-preempt", "delete high-availability vrrp group g1 rfc3768-compatibility", "delete high-availability vrrp sync-group sg1 health-check failure-count 3", "set high-availability vrrp group g2 address 192.168.3.3", "set high-availability vrrp group g2 address 192.168.4.4 interface eth3", "delete high-availability disable", "delete high-availability vrrp snmp", "set high-availability virtual-server s1 address 10.10.10.5", "set high-availability virtual-server s1 real-server 10.10.50.2 port 8443", "set high-availability virtual-server s2 persistence-timeout 30", "set high-availability virtual-server s2 protocol tcp", "set high-availability virtual-server s3 address 10.10.10.3", "set high-availability virtual-server s3 port 88", "set high-availability virtual-server s3 protocol udp", "set high-availability vrrp global-parameters garp master-repeat 6", "set high-availability vrrp global-parameters startup-delay 32", "set high-availability vrrp sync-group sg1 health-check failure-count 5", ] self.execute_module(changed=True, commands=commands) - def test_ntp_replaced_idempotent(self): + def test_vrrp_replaced_idempotent(self): set_module_args( dict( config=dict( disable=True, virtual_servers=[ dict( address="10.10.10.1", algorithm="round-robin", delay_loop=60, forward_method="direct", fwmark=10, name="s1", persistence_timeout="30", protocol="tcp", real_server=[ dict( address="10.10.50.1", port=443, health_check_script="/var/tmp/script.sh", ), ], ), dict( address="10.10.10.2", name="s2", port=81, real_server=[ dict( address="real1", port=8081, connection_timeout=5, ), dict( address="real2", port=8080, ), ], ), ], vrrp=dict( global_parameters=dict( garp=dict( interval=30, master_delay=10, master_refresh=100, master_refresh_repeat=200, master_repeat=5, ), version="3", startup_delay=30, ), groups=[ dict( authentication=dict( password="testpass", type="plaintext-password", ), address=[ "1.1.1.1", "3.3.3.3", "5.5.5.5 interface eth2", ], advertise_interval=10, description="Group_1", disable=True, excluded_address=[ "192.168.1.8", "192.168.1.7 interface eth3", ], garp=dict( interval=20, master_delay=5, master_refresh=50, master_refresh_repeat=100, master_repeat=3, ), health_check=dict( failure_count=3, interval=10, ping="192.168.1.5", script="script.sh", ), hello_source_address="192.168.1.2", interface="eth2", name="g1", no_preempt=True, peer_address="192.168.1.3", priority=100, rfc3768_compatibility=True, track=dict( exclude_vrrp_interface=True, interface=["eth1"], ), transition_script=dict( backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", master="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), vrid=20, ), ], snmp="enabled", sync_groups=[ dict( health_check=dict( failure_count=3, interval=10, ping="192.168.1.1", script="/var/tmp/script.sh", ), member=["g1"], name="sg1", transition_script=dict( backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", master="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), ), ], ), ), state="replaced", ), ) self.execute_module(changed=False, commands=[]) def test_vrrp_overridden(self): set_module_args( dict( config=dict( disable=False, vrrp=dict( snmp="disabled", global_parameters=dict( startup_delay=32, version=3, garp=dict( interval=30, master_delay=11, master_refresh=100, master_refresh_repeat=200, master_repeat=6, ), ), groups=[ dict( name="g1", description="Group_1", interface="eth2", address="1.1.1.1", disable=True, no_preempt=True, rfc3768_compatibility=True, vrid=20, peer_address="192.168.1.3", advertise_interval=10, priority=100, garp=dict( interval=20, master_delay=5, master_refresh=50, master_refresh_repeat=100, master_repeat=3, ), authentication=dict( type="plaintext-password", password="testpass", ), transition_script=dict( master="/var/tmp/script.sh", backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), track=dict( exclude_vrrp_interface=True, ), ), dict( name="g2", address=[ "192.168.3.3", "192.168.4.4 interface eth3", ], ), ], sync_groups=[ dict( name="sg1", member=["g1", "g2"], transition_script=dict( master="/var/tmp/script.sh", backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), health_check=dict( failure_count=4, interval=10, ping="192.168.1.100", script="/var/tmp/script.sh", ), ), ], ), virtual_servers=[ dict( name="s1", address="10.10.10.15", algorithm="round-robin", delay_loop=60, fwmark=12, forward_method="direct", persistence_timeout=30, protocol="tcp", real_server=[ dict( address="10.10.10.1", connection_timeout=61, port=443, ), ], ), ], ), state="overridden", ), ) commands = [ "delete high-availability disable", "delete high-availability virtual-server s1", "delete high-availability vrrp global-parameters garp", "delete high-availability vrrp group g1", "delete high-availability vrrp sync-group sg1", "set high-availability virtual-server s1 address 10.10.10.15", "set high-availability virtual-server s1 algorithm round-robin", "set high-availability virtual-server s1 delay-loop 60", "set high-availability virtual-server s1 forward-method direct", "set high-availability virtual-server s1 fwmark 12", "set high-availability virtual-server s1 persistence-timeout 30", "set high-availability virtual-server s1 protocol tcp", "set high-availability virtual-server s1 real-server 10.10.10.1 connection-timeout 61", "set high-availability virtual-server s1 real-server 10.10.10.1 port 443", "set high-availability vrrp global-parameters garp interval 30", "set high-availability vrrp global-parameters garp master-delay 11", "set high-availability vrrp global-parameters garp master-refresh 100", "set high-availability vrrp global-parameters garp master-refresh-repeat 200", "set high-availability vrrp global-parameters garp master-repeat 6", "set high-availability vrrp global-parameters startup-delay 32", "set high-availability vrrp group g1 address 1.1.1.1", "set high-availability vrrp group g1 advertise-interval 10", "set high-availability vrrp group g1 authentication password testpass", "set high-availability vrrp group g1 authentication type plaintext-password", "set high-availability vrrp group g1 description 'Group_1'", "set high-availability vrrp group g1 disable", "set high-availability vrrp group g1 garp interval 20", "set high-availability vrrp group g1 garp master-delay 5", "set high-availability vrrp group g1 garp master-refresh 50", "set high-availability vrrp group g1 garp master-refresh-repeat 100", "set high-availability vrrp group g1 garp master-repeat 3", "set high-availability vrrp group g1 interface eth2", "set high-availability vrrp group g1 no-preempt", "set high-availability vrrp group g1 peer-address 192.168.1.3", "set high-availability vrrp group g1 priority 100", "set high-availability vrrp group g1 rfc3768-compatibility", "set high-availability vrrp group g1 track exclude-vrrp-interface", "set high-availability vrrp group g1 transition-script backup /var/tmp/script.sh", "set high-availability vrrp group g1 transition-script fault /var/tmp/script.sh", "set high-availability vrrp group g1 transition-script master /var/tmp/script.sh", "set high-availability vrrp group g1 transition-script stop /var/tmp/script.sh", "set high-availability vrrp group g1 vrid 20", "set high-availability vrrp group g2 address 192.168.3.3", "set high-availability vrrp group g2 address 192.168.4.4 interface eth3", "set high-availability vrrp sync-group sg1 health-check failure-count 4", "set high-availability vrrp sync-group sg1 health-check interval 10", "set high-availability vrrp sync-group sg1 health-check ping 192.168.1.100", "set high-availability vrrp sync-group sg1 health-check script /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 member g1", "set high-availability vrrp sync-group sg1 member g2", "set high-availability vrrp sync-group sg1 transition-script backup /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 transition-script fault /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 transition-script master /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 transition-script stop /var/tmp/script.sh", ] self.execute_module(changed=True, commands=commands) def test_vrrp_overridden_idempotent(self): set_module_args( dict( config=dict( disable=True, virtual_servers=[ dict( address="10.10.10.1", algorithm="round-robin", delay_loop=60, forward_method="direct", fwmark=10, name="s1", persistence_timeout="30", protocol="tcp", real_server=[ dict( address="10.10.50.1", port=443, health_check_script="/var/tmp/script.sh", ), ], ), dict( address="10.10.10.2", name="s2", port=81, real_server=[ dict( address="real1", port=8081, connection_timeout=5, ), dict( address="real2", port=8080, ), ], ), ], vrrp=dict( global_parameters=dict( garp=dict( interval=30, master_delay=10, master_refresh=100, master_refresh_repeat=200, master_repeat=5, ), version="3", startup_delay=30, ), groups=[ dict( authentication=dict( password="testpass", type="plaintext-password", ), address=[ "1.1.1.1", "3.3.3.3", "5.5.5.5 interface eth2", ], advertise_interval=10, description="Group_1", disable=True, excluded_address=[ "192.168.1.8", "192.168.1.7 interface eth3", ], garp=dict( interval=20, master_delay=5, master_refresh=50, master_refresh_repeat=100, master_repeat=3, ), health_check=dict( failure_count=3, interval=10, ping="192.168.1.5", script="script.sh", ), hello_source_address="192.168.1.2", interface="eth2", name="g1", no_preempt=True, peer_address="192.168.1.3", priority=100, rfc3768_compatibility=True, track=dict( exclude_vrrp_interface=True, interface=["eth1"], ), transition_script=dict( backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", master="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), vrid=20, ), ], snmp="enabled", sync_groups=[ dict( health_check=dict( failure_count=3, interval=10, ping="192.168.1.1", script="/var/tmp/script.sh", ), member=["g1"], name="sg1", transition_script=dict( backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", master="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), ), ], ), ), state="overridden", ), ) self.execute_module(changed=False, commands=[]) def test_vrrp_rendered(self): set_module_args( dict( config=dict( virtual_servers=[ dict( name="s1", address="10.10.10.1", algorithm="round-robin", delay_loop=60, forward_method="direct", fwmark=10, persistence_timeout=30, protocol="tcp", real_server=[ dict( address="10.10.50.1", health_check_script="/var/tmp/script.sh", port=443, ), ], ), dict( name="s2", address="10.10.10.2", port=81, real_server=[ dict( address="real1", connection_timeout=5, port=8081, ), dict( address="real2", port=8080, ), ], ), ], vrrp=dict( snmp="enabled", global_parameters=dict( startup_delay=31, version="3", garp=dict( interval=30, master_delay=10, master_refresh=100, master_refresh_repeat=200, master_repeat=5, ), ), groups=[ dict( name="g1", description="Group_1", interface="eth2", address="1.1.1.1", advertise_interval=10, peer_address="192.168.1.3", priority=100, disable=True, no_preempt=True, rfc3768_compatibility=True, vrid=20, excluded_address=[ "192.168.1.7 interface eth3", ], garp=dict( interval=20, master_delay=5, master_refresh=50, master_refresh_repeat=100, master_repeat=3, ), authentication=dict( type="plaintext-password", password="testpass", ), transition_script=dict( master="/var/tmp/script.sh", backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), track=dict( exclude_vrrp_interface=True, interface=[ "eth0", ], ), ), dict( name="g2", description="Group_2", interface="eth1", address=[ "2.2.2.2", "2.2.2.3 interface eth3", ], disable=False, no_preempt=False, rfc3768_compatibility=False, vrid=11, health_check=dict( failure_count=5, interval=15, ping="192.168.1.100", script="/var/tmp/script.sh", ), hello_source_address="2.2.2.2", ), ], sync_groups=[ dict( name="sg1", member=[ "g1", ], transition_script=dict( master="/var/tmp/script.sh", backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), health_check=dict( failure_count=3, interval=10, ping="192.168.2.100", script="/var/tmp/script.sh", ), ), ], ), ), state="rendered", ), ) rendered_commands = [ "set high-availability virtual-server s1 address 10.10.10.1", "set high-availability virtual-server s1 algorithm round-robin", "set high-availability virtual-server s1 delay-loop 60", "set high-availability virtual-server s1 forward-method direct", "set high-availability virtual-server s1 fwmark 10", "set high-availability virtual-server s1 persistence-timeout 30", "set high-availability virtual-server s1 protocol tcp", "set high-availability virtual-server s1 real-server 10.10.50.1 health-check script /var/tmp/script.sh", "set high-availability virtual-server s1 real-server 10.10.50.1 port 443", "set high-availability virtual-server s2 address 10.10.10.2", "set high-availability virtual-server s2 port 81", "set high-availability virtual-server s2 real-server real1 connection-timeout 5", "set high-availability virtual-server s2 real-server real1 port 8081", "set high-availability virtual-server s2 real-server real2 port 8080", "set high-availability vrrp global-parameters garp interval 30", "set high-availability vrrp global-parameters garp master-delay 10", "set high-availability vrrp global-parameters garp master-refresh 100", "set high-availability vrrp global-parameters garp master-refresh-repeat 200", "set high-availability vrrp global-parameters garp master-repeat 5", "set high-availability vrrp global-parameters startup-delay 31", "set high-availability vrrp global-parameters version 3", "set high-availability vrrp group g1 address 1.1.1.1", "set high-availability vrrp group g1 advertise-interval 10", "set high-availability vrrp group g1 authentication password testpass", "set high-availability vrrp group g1 authentication type plaintext-password", "set high-availability vrrp group g1 description 'Group_1'", "set high-availability vrrp group g1 disable", "set high-availability vrrp group g1 excluded-address 192.168.1.7 interface eth3", "set high-availability vrrp group g1 garp interval 20", "set high-availability vrrp group g1 garp master-delay 5", "set high-availability vrrp group g1 garp master-refresh 50", "set high-availability vrrp group g1 garp master-refresh-repeat 100", "set high-availability vrrp group g1 garp master-repeat 3", "set high-availability vrrp group g1 interface eth2", "set high-availability vrrp group g1 no-preempt", "set high-availability vrrp group g1 peer-address 192.168.1.3", "set high-availability vrrp group g1 priority 100", "set high-availability vrrp group g1 rfc3768-compatibility", "set high-availability vrrp group g1 track exclude-vrrp-interface", "set high-availability vrrp group g1 track interface eth0", "set high-availability vrrp group g1 transition-script backup /var/tmp/script.sh", "set high-availability vrrp group g1 transition-script fault /var/tmp/script.sh", "set high-availability vrrp group g1 transition-script master /var/tmp/script.sh", "set high-availability vrrp group g1 transition-script stop /var/tmp/script.sh", "set high-availability vrrp group g1 vrid 20", "set high-availability vrrp group g2 address 2.2.2.2", "set high-availability vrrp group g2 address 2.2.2.3 interface eth3", "set high-availability vrrp group g2 description 'Group_2'", "set high-availability vrrp group g2 health-check failure-count 5", "set high-availability vrrp group g2 health-check interval 15", "set high-availability vrrp group g2 health-check ping 192.168.1.100", "set high-availability vrrp group g2 health-check script /var/tmp/script.sh", "set high-availability vrrp group g2 hello-source-address 2.2.2.2", "set high-availability vrrp group g2 interface eth1", "set high-availability vrrp group g2 vrid 11", "set high-availability vrrp snmp", "set high-availability vrrp sync-group sg1 health-check failure-count 3", "set high-availability vrrp sync-group sg1 health-check interval 10", "set high-availability vrrp sync-group sg1 health-check ping 192.168.2.100", "set high-availability vrrp sync-group sg1 health-check script /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 member g1", "set high-availability vrrp sync-group sg1 transition-script backup /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 transition-script fault /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 transition-script master /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 transition-script stop /var/tmp/script.sh", ] result = self.execute_module(changed=False) self.assertEqual( sorted(result["rendered"]), sorted(rendered_commands), result["rendered"], ) def test_vrrp_parsed(self): commands = ( "set high-availability disable", "set high-availability virtual-server s1 address 10.10.10.1", "set high-availability virtual-server s1 algorithm round-robin", "set high-availability virtual-server s1 delay-loop 60", "set high-availability virtual-server s1 forward-method direct", "set high-availability virtual-server s1 fwmark 10", "set high-availability virtual-server s1 persistence-timeout 30", "set high-availability virtual-server s1 protocol tcp", "set high-availability virtual-server s1 real-server 10.10.50.1 health-check script '/var/tmp/script.sh'", "set high-availability virtual-server s1 real-server 10.10.50.1 port 443", "set high-availability virtual-server s2 address 10.10.10.2", "set high-availability virtual-server s2 port 81", "set high-availability virtual-server s2 real-server real1 connection-timeout 5", "set high-availability virtual-server s2 real-server real1 port 8081", "set high-availability virtual-server s2 real-server real2 port 8080", "set high-availability vrrp global-parameters garp interval 30", "set high-availability vrrp global-parameters garp master-delay 10", "set high-availability vrrp global-parameters garp master-refresh 100", "set high-availability vrrp global-parameters garp master-refresh-repeat 200", "set high-availability vrrp global-parameters garp master-repeat 5", "set high-availability vrrp global-parameters startup-delay 30", "set high-availability vrrp global-parameters version 3", "set high-availability vrrp group g1 address '1.1.1.1'", "set high-availability vrrp group g1 advertise-interval 10", "set high-availability vrrp group g1 authentication password 'testpass'", "set high-availability vrrp group g1 authentication type 'plaintext-password'", "set high-availability vrrp group g1 description 'Group_1'", "set high-availability vrrp group g1 disable", "set high-availability vrrp group g1 excluded-address '192.168.1.7' interface 'eth3'", "set high-availability vrrp group g1 excluded-address '192.168.1.8'", "set high-availability vrrp group g1 garp interval 20", "set high-availability vrrp group g1 garp master-delay 5", "set high-availability vrrp group g1 garp master-refresh 50", "set high-availability vrrp group g1 garp master-refresh-repeat 100", "set high-availability vrrp group g1 garp master-repeat 3", "set high-availability vrrp group g1 health-check failure-count 3", "set high-availability vrrp group g1 health-check interval 10", "set high-availability vrrp group g1 health-check ping '192.168.1.5'", "set high-availability vrrp group g1 health-check script 'script.sh'", "set high-availability vrrp group g1 hello-source-address '192.168.1.2'", "set high-availability vrrp group g1 interface 'eth2'", "set high-availability vrrp group g1 no-preempt", "set high-availability vrrp group g1 peer-address '192.168.1.3'", "set high-availability vrrp group g1 priority 100", "set high-availability vrrp group g1 rfc3768-compatibility", "set high-availability vrrp group g1 track exclude-vrrp-interface", "set high-availability vrrp group g1 track interface 'eth1'", "set high-availability vrrp group g1 transition-script backup '/var/tmp/script.sh'", "set high-availability vrrp group g1 transition-script fault '/var/tmp/script.sh'", "set high-availability vrrp group g1 transition-script master '/var/tmp/script.sh'", "set high-availability vrrp group g1 transition-script stop '/var/tmp/script.sh'", "set high-availability vrrp group g1 vrid 20", "set high-availability vrrp group g2 address '192.168.3.3'", "set high-availability vrrp group g2 address '192.168.4.4' interface 'eth3'", "set high-availability vrrp snmp", "set high-availability vrrp sync-group sg1 health-check failure-count 3", "set high-availability vrrp sync-group sg1 health-check interval 10", "set high-availability vrrp sync-group sg1 health-check ping '192.168.1.1'", "set high-availability vrrp sync-group sg1 health-check script '/var/tmp/script.sh'", "set high-availability vrrp sync-group sg1 member 'g1'", "set high-availability vrrp sync-group sg1 transition-script backup '/var/tmp/script.sh'", "set high-availability vrrp sync-group sg1 transition-script fault '/var/tmp/script.sh'", "set high-availability vrrp sync-group sg1 transition-script master '/var/tmp/script.sh'", "set high-availability vrrp sync-group sg1 transition-script stop '/var/tmp/script.sh'", ) parsed_str = "\n".join(commands) set_module_args(dict(running_config=parsed_str, state="parsed")) result = self.execute_module(changed=False) parsed_list = { "disable": True, "virtual_servers": [ { "name": "s1", "address": "10.10.10.1", "algorithm": "round-robin", "delay_loop": 60, "forward_method": "direct", "fwmark": 10, "persistence_timeout": 30, "protocol": "tcp", "real_server": [ { "address": "10.10.50.1", "health_check_script": "/var/tmp/script.sh", "port": 443, }, ], }, { "name": "s2", "address": "10.10.10.2", "port": 81, "real_server": [ { "address": "real1", "connection_timeout": 5, "port": 8081, }, { "address": "real2", "port": 8080, }, ], }, ], "vrrp": { "snmp": "enabled", "global_parameters": { "startup_delay": 30, "version": "3", "garp": { "interval": 30, "master_delay": 10, "master_refresh": 100, "master_refresh_repeat": 200, "master_repeat": 5, }, }, "groups": [ { "name": "g1", "description": "Group_1", "disable": True, "no_preempt": True, "rfc3768_compatibility": True, "interface": "eth2", "address": "1.1.1.1", "advertise_interval": 10, "peer_address": "192.168.1.3", "priority": 100, "vrid": 20, "garp": { "interval": 20, "master_delay": 5, "master_refresh": 50, "master_refresh_repeat": 100, "master_repeat": 3, }, "authentication": { "type": "plaintext-password", "password": "testpass", }, "transition_script": { "master": "/var/tmp/script.sh", "backup": "/var/tmp/script.sh", "fault": "/var/tmp/script.sh", "stop": "/var/tmp/script.sh", }, "health_check": { "failure_count": 3, "interval": 10, "ping": "192.168.1.5", "script": "script.sh", }, "track": { "exclude_vrrp_interface": True, "interface": ["eth1"], }, "excluded_address": [ "192.168.1.7 interface eth3", "192.168.1.8", ], "hello_source_address": "192.168.1.2", }, ], "sync_groups": [ { "name": "sg1", "member": ["g1"], "transition_script": { "master": "/var/tmp/script.sh", "backup": "/var/tmp/script.sh", "fault": "/var/tmp/script.sh", "stop": "/var/tmp/script.sh", }, "health_check": { "failure_count": 3, "interval": 10, "ping": "192.168.1.1", "script": "/var/tmp/script.sh", }, }, ], }, } self.assertEqual(sorted(parsed_list), sorted(result["parsed"])) def test_vrrp_gathered(self): set_module_args(dict(state="gathered")) result = self.execute_module(changed=False) gathered_list = { "disable": True, "virtual_servers": [ { "name": "s1", "address": "10.10.10.1", "algorithm": "round-robin", "delay_loop": 60, "forward_method": "direct", "fwmark": 10, "persistence_timeout": 30, "protocol": "tcp", "real_server": [ { "address": "10.10.50.1", "health_check_script": "/var/tmp/script.sh", "port": 443, }, ], }, { "name": "s2", "address": "10.10.10.2", "port": 81, "real_server": [ { "address": "real1", "connection_timeout": 5, "port": 8081, }, { "address": "real2", "port": 8080, }, ], }, ], "vrrp": { "snmp": "enabled", "global_parameters": { "startup_delay": 30, "version": "3", "garp": { "interval": 30, "master_delay": 10, "master_refresh": 100, "master_refresh_repeat": 200, "master_repeat": 5, }, }, "groups": [ { "name": "g1", "description": "Group_1", "disable": True, "no_preempt": True, "rfc3768_compatibility": True, "interface": "eth2", "address": "1.1.1.1", "advertise_interval": 10, "peer_address": "192.168.1.3", "priority": 100, "vrid": 20, "garp": { "interval": 20, "master_delay": 5, "master_refresh": 50, "master_refresh_repeat": 100, "master_repeat": 3, }, "authentication": { "type": "plaintext-password", "password": "testpass", }, "transition_script": { "master": "/var/tmp/script.sh", "backup": "/var/tmp/script.sh", "fault": "/var/tmp/script.sh", "stop": "/var/tmp/script.sh", }, "health_check": { "failure_count": 3, "interval": 10, "ping": "192.168.1.5", "script": "script.sh", }, "track": { "exclude_vrrp_interface": True, "interface": ["eth1"], }, "excluded_address": [ "192.168.1.7 interface eth3", "192.168.1.8", ], "hello_source_address": "192.168.1.2", }, ], "sync_groups": [ { "name": "sg1", "member": ["g1"], "transition_script": { "master": "/var/tmp/script.sh", "backup": "/var/tmp/script.sh", "fault": "/var/tmp/script.sh", "stop": "/var/tmp/script.sh", }, "health_check": { "failure_count": 3, "interval": 10, "ping": "192.168.1.1", "script": "/var/tmp/script.sh", }, }, ], }, } self.assertEqual(sorted(gathered_list), sorted(result["gathered"])) def test_vrrp_groups_deleted(self): set_module_args( dict( config=dict( vrrp=dict( groups=[ dict(name="g1"), ], sync_groups=[ dict(name="sg1"), ], ), ), state="deleted", ), ) commands = [ "delete high-availability vrrp group g1", "delete high-availability vrrp sync-group sg1", ] self.execute_module(changed=True, commands=commands) def test_vrrp_objs_deleted(self): set_module_args( dict( config=dict( disable=False, vrrp=dict( global_parameters=dict( startup_delay=32, garp=dict(), ), groups=[ dict(name="g1"), ], sync_groups=[], ), virtual_servers=[], ), state="deleted", ), ) commands = [ "delete high-availability virtual-server", "delete high-availability vrrp global-parameters garp", "delete high-availability vrrp global-parameters startup-delay", "delete high-availability vrrp group g1", "delete high-availability vrrp sync-group", ] self.execute_module(changed=True, commands=commands) def test_vrrp_deleted_all(self): set_module_args( dict( config=dict(), state="deleted", ), ) commands = [ "delete high-availability", ] self.execute_module(changed=True, commands=commands) def test_vrrp_purged(self): set_module_args( dict( config=dict(), state="purged", ), ) commands = [ "delete high-availability", ] self.execute_module(changed=True, commands=commands)