From a6adced609472d88cb525f2ef1a4054b52692f76 Mon Sep 17 00:00:00 2001 From: omnom62 <75066712+omnom62@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:21:24 +1000 Subject: T6820: HA/VRRP module (#438) * Init VRRP * RM Templates * rm_templates for global_parameters * virtual servers rm_templates * update * sync-group rm_tempalte * VRRP GP GARP template rework * RM template and new facts logic * vrrp argspec * Suppressing backup versions of files * Facts for vrrp * VRRP facts updates * Facts almost done * Argspec vrrp * Argspec completed * Argspec and facts for VRRP finished * Documentation for VRRP module * Sanity tests fixes and documentation * VRRP config started * Doc fixes * Doc fixes * Doc fixes * Configuration for VRRP * wip * Updates to facts, config, templates in VRRP * VRRP groups templates, facts and config * VRRP works - templates for groups and virtual_servers * Mid-result * VRRP VS prototype * Groups processor update * Reworked vrrp.group and vrrp.group.garp processors * VRRP grp recursive * VRRP groups and vserver contd * Update to VRRP want processing * Updates to VRRP funcs * VRRP VS RS config * Sanity test fixes midway * VRRP Real server fixes * VRRP sync_group * Latest fixes to etire VRRP config parsing * Midway compare function * update to vrrp group processing * Mid results for merge parsers / compare * Working merge for vrrp and virtserv * Final merge / compare * WIP replaced / Delete * Updates to state processing * Mid-result for virtual_servers * Mid result commit * Testing high-availability vrrp processing * Retesting vrrp stanza * Mid results * Real server parsing * SNMP vrrp fixed * Sanity fixes * Purged, rendered, gathered, parsed action states for VRRP * VRRP integration and unit tests; code fixes midway results * Sanity check fixes * Merged integration test case for VRRP * VRRP rendered integration test case * Sanity tests fixes * VRRP unit tests - merged and purged * Mid-way replaced, deleted and overriden work. Description string fix * Updates to integration and unit tests for VRRP * Mid-result for deleted state VRRP * Mid-results for VRRP deleted * Deleted vrrp midway results * Deleted state function for VRRP * Deleted state VRRP, sanity checks fix * Deleted integration tests * Deleted_all unit and integration tests * Deleted_all unit and integration tests * Sanity check fixes * Mid-results overridden VRRP * WIP vrrp overridden sit and uat * VRRP overridden sanity test * UAT overriden vrrp * UAT deleted for vrrp * VRRP replaced UAT and SIT * VRRP RTT SIT * VRRP SIT suite ready * Finals for VRRP module - clean-up and fixed UAT * VRRP module doc examples updated * VRRP sanity tests fixes * VRRP disbale default fix * Misc lint fixes * Misc Lint fixes * remove old files * Rename vyos_vrrp -> vyos_ha * Rename vyos_vrrp to vyos_ha * doc fixes * Changes to HA module * HA module fixes * Sanity test fixes * changelog * ignore for HA/VRRP and 2.22 * AI-generated comments and fixes * Fixes after review * Misc fixes * Misc fixes * Misc fixes * Misc fixes * Sanity fixes * Sanity fixes * Misc changes * T6820: code refactor to address valid AI comments * T6820: Optimising facts, config and rm_templates * T6820: UAT fixes * T6820: docstring for utils/combine() * T6820 Misc fixes to changelog --------- Co-authored-by: Gaige B Paulsen --- .gitignore | 1 + README.md | 1 + changelogs/fragments/t6820_vrrp.yml | 3 + docs/vyos.vyos.vyos_ha_module.rst | 2523 ++++++++++++++++++++ meta/runtime.yml | 2 + plugins/action/ha.py | 1 + .../network/vyos/argspec/ha/__init__.py | 0 plugins/module_utils/network/vyos/argspec/ha/ha.py | 196 ++ .../network/vyos/config/ha/__init__.py | 0 plugins/module_utils/network/vyos/config/ha/ha.py | 705 ++++++ plugins/module_utils/network/vyos/facts/facts.py | 4 + .../module_utils/network/vyos/facts/ha/__init__.py | 0 plugins/module_utils/network/vyos/facts/ha/ha.py | 174 ++ .../module_utils/network/vyos/rm_templates/ha.py | 1011 ++++++++ plugins/module_utils/network/vyos/utils/utils.py | 54 +- plugins/modules/vyos_ha.py | 1327 ++++++++++ .../integration/targets/vyos_ha/defaults/main.yaml | 3 + tests/integration/targets/vyos_ha/tasks/cli.yaml | 20 + tests/integration/targets/vyos_ha/tasks/main.yaml | 5 + .../vyos_ha/tests/cli/_add_dummy_scripts.yaml | 6 + .../targets/vyos_ha/tests/cli/_parsed.cfg | 69 + .../vyos_ha/tests/cli/_populate_config.yaml | 8 + .../targets/vyos_ha/tests/cli/_remove_config.yaml | 15 + .../targets/vyos_ha/tests/cli/deleted_all.yaml | 44 + .../targets/vyos_ha/tests/cli/deleted_bulk.yaml | 50 + .../vyos_ha/tests/cli/deleted_vrrp_groups.yaml | 45 + .../targets/vyos_ha/tests/cli/empty_config.yaml | 58 + .../targets/vyos_ha/tests/cli/gathered.yaml | 25 + .../targets/vyos_ha/tests/cli/merged.yaml | 42 + .../targets/vyos_ha/tests/cli/overridden.yaml | 35 + .../targets/vyos_ha/tests/cli/parsed.yaml | 15 + .../targets/vyos_ha/tests/cli/purged.yaml | 44 + .../targets/vyos_ha/tests/cli/rendered.yaml | 15 + .../targets/vyos_ha/tests/cli/replaced.yaml | 37 + .../integration/targets/vyos_ha/tests/cli/rtt.yaml | 39 + tests/integration/targets/vyos_ha/vars/main.yaml | 787 ++++++ tests/sanity/ignore-2.15.txt | 1 + tests/sanity/ignore-2.16.txt | 1 + tests/sanity/ignore-2.17.txt | 1 + tests/sanity/ignore-2.18.txt | 1 + tests/sanity/ignore-2.19.txt | 1 + tests/sanity/ignore-2.20.txt | 1 + tests/sanity/ignore-2.21.txt | 1 + tests/sanity/ignore-2.22.txt | 1 + .../network/vyos/fixtures/vyos_ha_config.cfg | 64 + tests/unit/modules/network/vyos/test_vyos_ha.py | 1405 +++++++++++ 46 files changed, 8833 insertions(+), 8 deletions(-) create mode 100644 changelogs/fragments/t6820_vrrp.yml create mode 100644 docs/vyos.vyos.vyos_ha_module.rst create mode 120000 plugins/action/ha.py create mode 100644 plugins/module_utils/network/vyos/argspec/ha/__init__.py create mode 100644 plugins/module_utils/network/vyos/argspec/ha/ha.py create mode 100644 plugins/module_utils/network/vyos/config/ha/__init__.py create mode 100644 plugins/module_utils/network/vyos/config/ha/ha.py create mode 100644 plugins/module_utils/network/vyos/facts/ha/__init__.py create mode 100644 plugins/module_utils/network/vyos/facts/ha/ha.py create mode 100644 plugins/module_utils/network/vyos/rm_templates/ha.py create mode 100644 plugins/modules/vyos_ha.py create mode 100644 tests/integration/targets/vyos_ha/defaults/main.yaml create mode 100644 tests/integration/targets/vyos_ha/tasks/cli.yaml create mode 100644 tests/integration/targets/vyos_ha/tasks/main.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/_add_dummy_scripts.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/_parsed.cfg create mode 100644 tests/integration/targets/vyos_ha/tests/cli/_populate_config.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/_remove_config.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/deleted_all.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/deleted_bulk.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/deleted_vrrp_groups.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/empty_config.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/gathered.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/merged.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/overridden.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/parsed.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/purged.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/rendered.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/replaced.yaml create mode 100644 tests/integration/targets/vyos_ha/tests/cli/rtt.yaml create mode 100644 tests/integration/targets/vyos_ha/vars/main.yaml create mode 100644 tests/unit/modules/network/vyos/fixtures/vyos_ha_config.cfg create mode 100644 tests/unit/modules/network/vyos/test_vyos_ha.py diff --git a/.gitignore b/.gitignore index 1dbe8b0e..2f5df4fe 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,7 @@ venv.bak/ *.code-workspace .vscode/ .DS_Store +*.bak changelogs/.plugin-cache.yaml # inventory for testing diff --git a/README.md b/README.md index 0a4b8f48..3e54f27c 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Name | Description [vyos.vyos.vyos_firewall_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_firewall_global_module.rst)|Firewall global resource module [vyos.vyos.vyos_firewall_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_firewall_interfaces_module.rst)|Firewall interfaces resource module [vyos.vyos.vyos_firewall_rules](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_firewall_rules_module.rst)|Firewall rules resource module +[vyos.vyos.vyos_ha](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ha_module.rst)|Manage VRRP and load balancer configuration on VyOS [vyos.vyos.vyos_hostname](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_hostname_module.rst)|Manages hostname resource module [vyos.vyos.vyos_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_interfaces_module.rst)|Manages interface attributes of VyOS network devices. [vyos.vyos.vyos_l3_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_l3_interfaces_module.rst)|Layer 3 interfaces resource module. diff --git a/changelogs/fragments/t6820_vrrp.yml b/changelogs/fragments/t6820_vrrp.yml new file mode 100644 index 00000000..94145121 --- /dev/null +++ b/changelogs/fragments/t6820_vrrp.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - vyos_ha - Add VRRP (High Availability) support. diff --git a/docs/vyos.vyos.vyos_ha_module.rst b/docs/vyos.vyos.vyos_ha_module.rst new file mode 100644 index 00000000..5e03661c --- /dev/null +++ b/docs/vyos.vyos.vyos_ha_module.rst @@ -0,0 +1,2523 @@ +.. _vyos.vyos.vyos_ha_module: + + +***************** +vyos.vyos.vyos_ha +***************** + +**Manage VRRP and load balancer configuration on VyOS** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module configures VRRP groups, global VRRP parameters, VRRP sync groups, and LVS-style virtual servers on VyOS 1.4+. +- Supports creation, modification, deletion, replacement, rendering, and parsing of VRRP-related configuration. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ config + +
+ dictionary +
+
+ +
Full VRRP and virtual server configuration.
+
+
+ disable + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Disable all VRRP and L4-LB configuration under this module.
+
+
+ virtual_servers + +
+ list + / elements=dictionary +
+
+ +
List of load balancer virtual server (LVS) definitions.
+
+
+ address + +
+ string +
+
+ +
Virtual IP address for the server.
+
+
+ algorithm + +
+ string +
+
+ +
Load balancing algorithm used for dispatching connections.
+
+
+ delay_loop + +
+ integer +
+
+ +
Delay loop interval in seconds.
+
+
+ forward_method + +
+ string +
+
+
    Choices: +
  • direct
  • +
  • nat
  • +
+
+
Forwarding method used by LVS.
+
+
+ fwmark + +
+ integer +
+
+ +
Firewall mark for LVS traffic classification.
+
+
+ name + +
+ string + / required +
+
+ +
Unique identifier for the virtual server.
+
+
+ persistence_timeout + +
+ integer +
+
+ +
Client persistence timeout in seconds.
+
+
+ port + +
+ integer +
+
+ +
TCP/UDP port provided by the virtual service.
+
+
+ protocol + +
+ string +
+
+
    Choices: +
  • tcp
  • +
  • udp
  • +
+
+
Transport protocol for the virtual server.
+
+
+ real_server + +
+ list + / elements=dictionary +
+
+ +
Backend real servers behind the virtual service.
+
+
+ address + +
+ string + / required +
+
+ +
Real server IP address.
+
+
+ connection_timeout + +
+ integer +
+
+ +
Backend server connection timeout.
+
+
+ health_check_script + +
+ string +
+
+ +
Path to health check script used for backend validation.
+
+
+ port + +
+ integer +
+
+ +
Backend server port.
+
+
+ vrrp + +
+ dictionary +
+
+ +
VRRP configuration including groups, global parameters, SNMP settings, and sync-groups.
+
+
+ global_parameters + +
+ dictionary +
+
+ +
Global VRRP tuning parameters.
+
+
+ garp + +
+ dictionary +
+
+ +
Gratuitous ARP related configuration.
+
+
+ interval + +
+ integer +
+
+ +
GARP interval in seconds.
+
+
+ master_delay + +
+ integer +
+
+ +
Delay before sending GARP as master.
+
+
+ master_refresh + +
+ integer +
+
+ +
Refresh interval for master GARP announcements.
+
+
+ master_refresh_repeat + +
+ integer +
+
+ +
Number of times to repeat refresh announcements.
+
+
+ master_repeat + +
+ integer +
+
+ +
Number of GARP repeats when transitioning to master.
+
+
+ startup_delay + +
+ integer +
+
+ +
Delay before VRRP starts after boot.
+
+
+ version + +
+ string +
+
+ +
VRRP protocol version.
+
+
+ groups + +
+ list + / elements=dictionary +
+
+ +
VRRP instance configuration groups.
+
+
+ address + +
+ list + / elements=string +
+
+ +
Virtual router IP addresses.
+
+
+ advertise_interval + +
+ integer +
+
+ +
VRRP advertisement interval.
+
+
+ authentication + +
+ dictionary +
+
+ +
VRRP group authentication options.
+
+
+ password + +
+ string +
+
+ +
Authentication password.
+
+
+ type + +
+ string +
+
+ +
Authentication type.
+
+
+ description + +
+ string +
+
+ +
Text description for the VRRP group.
+
+
+ disable + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Disable this VRRP group.
+
+
+ excluded_address + +
+ list + / elements=string +
+
+ +
IP address excluded from source checks.
+
+
+ garp + +
+ dictionary +
+
+ +
GARP-specific settings for this group.
+
+
+ interval + +
+ integer +
+
+ +
GARP interval.
+
+
+ master_delay + +
+ integer +
+
+ +
GARP master delay.
+
+
+ master_refresh + +
+ integer +
+
+ +
GARP master refresh interval.
+
+
+ master_refresh_repeat + +
+ integer +
+
+ +
Repeated refresh sends.
+
+
+ master_repeat + +
+ integer +
+
+ +
GARP repeat count.
+
+
+ health_check + +
+ dictionary +
+
+ +
VRRP group health check options.
+
+
+ failure_count + +
+ integer +
+
+ +
Allowed number of failed checks.
+
+
+ interval + +
+ integer +
+
+ +
Health check interval.
+
+
+ ping + +
+ string +
+
+ +
Host to ping for checks.
+
+
+ script + +
+ string +
+
+ +
Script to execute for health checking.
+
+
+ hello_source_address + +
+ string +
+
+ +
Source address for VRRP hello packets.
+
+
+ interface + +
+ string +
+
+ +
Interface used by the VRRP group.
+
+
+ name + +
+ string + / required +
+
+ +
VRRP group name.
+
+
+ no_preempt + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Disable preemption.
+
+
+ peer_address + +
+ string +
+
+ +
Peer VRRP router address.
+
+
+ preempt_delay + +
+ integer +
+
+ +
Delay before taking master role.
+
+
+ priority + +
+ integer +
+
+ +
VRRP priority (higher = preferred master).
+
+
+ rfc3768_compatibility + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Enable or disable RFC3768 compatibility mode.
+
+
+ track + +
+ dictionary +
+
+ +
Track interface and VRRP behaviour.
+
+
+ exclude_vrrp_interface + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+
Exclude VRRP interface from tracking.
+
+
+ interface + +
+ list + / elements=string +
+
+ +
Interface to track.
+
+
+ transition_script + +
+ dictionary +
+
+ +
Scripts executed during VRRP state transitions.
+
+
+ backup + +
+ string +
+
+ +
Path to backup script.
+
+
+ fault + +
+ string +
+
+ +
Path to fault script.
+
+
+ master + +
+ string +
+
+ +
Path to master script.
+
+
+ stop + +
+ string +
+
+ +
Path to stop script.
+
+
+ vrid + +
+ integer +
+
+ +
VRRP Virtual Router ID.
+
+
+ snmp + +
+ string +
+
+
    Choices: +
  • enabled
  • +
  • disabled
  • +
+
+
Enable SNMP support for VRRP.
+
+
+ sync_groups + +
+ list + / elements=dictionary +
+
+ +
VRRP sync-groups for coordinated failover.
+
+
+ health_check + +
+ dictionary +
+
+ +
Health check options for sync group.
+
+
+ failure_count + +
+ integer +
+
+ +
Allowed number of failures.
+
+
+ interval + +
+ integer +
+
+ +
Health check interval.
+
+
+ ping + +
+ string +
+
+ +
Host to ping.
+
+
+ script + +
+ string +
+
+ +
Script to run for health checking.
+
+
+ member + +
+ list + / elements=string +
+
+ +
List of VRRP groups participating in this sync group.
+
+
+ name + +
+ string + / required +
+
+ +
Sync-group name.
+
+
+ transition_script + +
+ dictionary +
+
+ +
Transition scripts for sync group events.
+
+
+ backup + +
+ string +
+
+ +
Backup state script.
+
+
+ fault + +
+ string +
+
+ +
Fault state script.
+
+
+ master + +
+ string +
+
+ +
Master state script.
+
+
+ stop + +
+ string +
+
+ +
Stop state script.
+
+
+ running_config + +
+ string +
+
+ +
Used only when state=parsed. Must contain the output of show configuration commands | grep high-availability.
+
+
+ state + +
+ string +
+
+
    Choices: +
  • deleted
  • +
  • merged ←
  • +
  • purged
  • +
  • replaced
  • +
  • gathered
  • +
  • rendered
  • +
  • parsed
  • +
  • overridden
  • +
+
+
Desired end state of the VRRP configuration.
+
+
+ + + + +Examples +-------- + +.. code-block:: yaml + + # Using merged + # Before state + + # vyos@vyos:~$ show configuration commands | match "set high-availability" + # vyos@vyos:~$ + + - name: Merge provided configuration with device configuration + vyos.vyos.vyos_ha: + config: + disable: true + virtual_servers: + - name: s1 + address: 10.10.10.5 + algorithm: round-robin + real_server: + - address: 10.10.50.2 + port: 8443 + - name: s2 + address: 10.10.10.2 + persistence_timeout: 30 + port: 81 + protocol: tcp + - name: s3 + address: 10.10.10.3 + port: 88 + protocol: udp + vrrp: + snmp: enabled + global_parameters: + startup_delay: 30 + garp: + master_repeat: 6 + groups: + - name: "g1" + peer_address: 192.168.1.3 + priority: 100 + disable: false + no_preempt: false + vrid: 20 + sync_groups: + - name: "sg1" + health_check: + failure_count: 5 + state: merged + + # After State + # vyos@vyos:~$ show configuration commands | match "set high-availability" + # set high-availability disable + # set high-availability virtual-server s1 address '10.10.10.5' + # set high-availability virtual-server s1 algorithm 'round-robin' + # set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' + # set high-availability virtual-server s2 address '10.10.10.2' + # set high-availability virtual-server s2 persistence-timeout '30' + # set high-availability virtual-server s2 port '81' + # 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 '30' + # 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 vrid '20' + # set high-availability vrrp snmp + # set high-availability vrrp sync-group sg1 health-check failure-count '5' + # vyos@vyos:~$ + # + # # Module Execution: + # + # "after": { + # "disable": true, + # "virtual_servers": [ + # { + # "address": "10.10.10.5", + # "algorithm": "round-robin", + # "name": "s1", + # "real_server": [ + # { + # "address": "10.10.50.2", + # "port": 8443 + # } + # ] + # }, + # { + # "address": "10.10.10.2", + # "name": "s2", + # "persistence_timeout": 30, + # "port": 81, + # "protocol": "tcp" + # }, + # { + # "address": "10.10.10.3", + # "name": "s3", + # "port": 88, + # "protocol": "udp" + # } + # ], + # "vrrp": { + # "global_parameters": { + # "garp": { + # "master_repeat": 6 + # }, + # "startup_delay": 30 + # }, + # "groups": [ + # { + # "disable": false, + # "name": "g1", + # "no_preempt": false, + # "peer_address": "192.168.1.3", + # "priority": 100, + # "rfc3768_compatibility": false, + # "vrid": 20 + # } + # ], + # "snmp": "enabled", + # "sync_groups": [ + # { + # "health_check": { + # "failure_count": 5 + # }, + # "name": "sg1" + # } + # ] + # } + # }, + # "before": { + # "disable": false + # }, + # "changed": true, + # "commands": [ + # "set high-availability disable", + # "set high-availability virtual-server s1 address 10.10.10.5", + # "set high-availability virtual-server s1 algorithm round-robin", + # "set high-availability virtual-server s1 real-server 10.10.50.2 port 8443", + # "set high-availability virtual-server s2 address 10.10.10.2", + # "set high-availability virtual-server s2 persistence-timeout 30", + # "set high-availability virtual-server s2 port 81", + # "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 30", + # "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 vrid 20", + # "set high-availability vrrp snmp", + # "set high-availability vrrp sync-group sg1 health-check failure-count 5" + # ], + + # Using replaced: + # -------------- + + # Before state: + # vyos@vyos:~$ show configuration commands | match "set high-availability" + # set high-availability disable + # set high-availability virtual-server s1 address '10.10.10.5' + # set high-availability virtual-server s1 algorithm 'round-robin' + # set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' + # set high-availability virtual-server s2 address '10.10.10.2' + # set high-availability virtual-server s2 persistence-timeout '30' + # set high-availability virtual-server s2 port '81' + # 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 '30' + # 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 vrid '20' + # set high-availability vrrp snmp + # set high-availability vrrp sync-group sg1 health-check failure-count '5' + # vyos@vyos:~$ + + - name: Replace + vyos.vyos.vyos_ha: + config: + disable: false + virtual_servers: + - name: s1 + address: 10.10.10.3 + algorithm: round-robin + port: 8443 + real_server: + - address: 10.10.50.3 + port: 8443 + - name: s2 + address: 10.10.10.2 + persistence_timeout: 300 + port: 81 + protocol: tcp + real_server: + - address: 10.10.50.30 + port: 8443 + - name: s3 + address: 10.10.10.3 + port: 88 + protocol: udp + real_server: + - address: 10.10.50.6 + port: 8443 + vrrp: + snmp: enabled + global_parameters: + startup_delay: 30 + garp: + master_repeat: 6 + groups: + - name: "g1" + peer_address: 192.168.1.13 + priority: 100 + disable: false + no_preempt: true + interface: eth1 + address: 192.168.51.13 + vrid: 20 + sync_groups: + - name: "sg1" + health_check: + failure_count: 3 + state: replaced + + # After state: + + # vyos@vyos:~$ show configuration commands | match "set high-availability" + # set high-availability virtual-server s1 address '10.10.10.3' + # set high-availability virtual-server s1 algorithm 'round-robin' + # set high-availability virtual-server s1 port '8443' + # set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' + # set high-availability virtual-server s1 real-server 10.10.50.3 port '8443' + # set high-availability virtual-server s2 address '10.10.10.2' + # set high-availability virtual-server s2 persistence-timeout '300' + # set high-availability virtual-server s2 port '81' + # set high-availability virtual-server s2 protocol 'tcp' + # set high-availability virtual-server s2 real-server 10.10.50.3 port '8443' + # 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 virtual-server s3 real-server 10.10.50.6 port '8443' + # set high-availability vrrp global-parameters garp master-repeat '6' + # set high-availability vrrp global-parameters startup-delay '30' + # set high-availability vrrp group g1 address 192.168.51.13 + # set high-availability vrrp group g1 interface 'eth1' + # 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 peer-address '192.168.1.13' + # set high-availability vrrp group g1 priority '100' + # set high-availability vrrp group g1 vrid '20' + # set high-availability vrrp snmp + # set high-availability vrrp sync-group sg1 health-check failure-count '3' + # vyos@vyos:~$ + # + # + # Module Execution: + # + # "after": { + # "disable": false, + # "virtual_servers": [ + # { + # "address": "10.10.10.3", + # "algorithm": "round-robin", + # "name": "s1", + # "port": 8443, + # "real_server": [ + # { + # "address": "10.10.50.2", + # "port": 8443 + # }, + # { + # "address": "10.10.50.3", + # "port": 8443 + # } + # ] + # }, + # { + # "address": "10.10.10.2", + # "name": "s2", + # "persistence_timeout": 300, + # "port": 81, + # "protocol": "tcp", + # "real_server": [ + # { + # "address": "10.10.50.3", + # "port": 8443 + # } + # ] + # }, + # { + # "address": "10.10.10.3", + # "name": "s3", + # "port": 88, + # "protocol": "udp", + # "real_server": [ + # { + # "address": "10.10.50.6", + # "port": 8443 + # } + # ] + # } + # ], + # "vrrp": { + # "global_parameters": { + # "garp": { + # "master_repeat": 6 + # }, + # "startup_delay": 30 + # }, + # "groups": [ + # { + # "address": "192.168.51.13", + # "disable": false, + # "interface": "eth1", + # "name": "g1", + # "no_preempt": true, + # "peer_address": "192.168.1.13", + # "priority": 100, + # "rfc3768_compatibility": false, + # "vrid": 20 + # } + # ], + # "snmp": "enabled", + # "sync_groups": [ + # { + # "health_check": { + # "failure_count": 3 + # }, + # "name": "sg1" + # } + # ] + # } + # }, + # "before": { + # "disable": true, + # "virtual_servers": [ + # { + # "address": "10.10.10.5", + # "algorithm": "round-robin", + # "name": "s1", + # "real_server": [ + # { + # "address": "10.10.50.2", + # "port": 8443 + # } + # ] + # }, + # { + # "address": "10.10.10.2", + # "name": "s2", + # "persistence_timeout": 30, + # "port": 81, + # "protocol": "tcp" + # }, + # { + # "address": "10.10.10.3", + # "name": "s3", + # "port": 88, + # "protocol": "udp" + # } + # ], + # "vrrp": { + # "global_parameters": { + # "garp": { + # "master_repeat": 6 + # }, + # "startup_delay": 30 + # }, + # "groups": [ + # { + # "disable": false, + # "name": "g1", + # "no_preempt": false, + # "peer_address": "192.168.1.3", + # "priority": 100, + # "rfc3768_compatibility": false, + # "vrid": 20 + # } + # ], + # "snmp": "enabled", + # "sync_groups": [ + # { + # "health_check": { + # "failure_count": 5 + # }, + # "name": "sg1" + # } + # ] + # } + # }, + # "changed": true, + # "commands": [ + # "delete high-availability disable", + # "set high-availability virtual-server s1 address 10.10.10.3", + # "set high-availability virtual-server s1 port 8443", + # "set high-availability virtual-server s1 real-server 10.10.50.3 port 8443", + # "set high-availability virtual-server s2 persistence-timeout 300", + # "set high-availability virtual-server s2 real-server 10.10.50.3 port 8443", + # "set high-availability virtual-server s3 real-server 10.10.50.6 port 8443", + # "set high-availability vrrp group g1 address 192.168.51.13", + # "set high-availability vrrp group g1 interface eth1", + # "set high-availability vrrp group g1 no-preempt", + # "set high-availability vrrp group g1 peer-address 192.168.1.13", + # "set high-availability vrrp sync-group sg1 health-check failure-count 3" + # ], + + # Using deleted: + # ------------- + + # Before state: + + # vyos@vyos:~$ show configuration commands | match "set high-availability" + # set high-availability disable + # set high-availability virtual-server s1 address '10.10.10.5' + # set high-availability virtual-server s1 algorithm 'round-robin' + # set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' + # set high-availability virtual-server s2 address '10.10.10.2' + # set high-availability virtual-server s2 persistence-timeout '30' + # set high-availability virtual-server s2 port '81' + # 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 '30' + # 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 vrid '20' + # set high-availability vrrp snmp + # set high-availability vrrp sync-group sg1 health-check failure-count '5' + # vyos@vyos:~$ + + - name: Delete configuration + vyos.vyos.vyos_ha: + config: + disable: false + vrrp: + snmp: disabled + global_parameters: + startup_delay: 32 + version: 3 + virtual_servers: + - name: 's1' + address: '10.10.10.1' + algorithm: 'round-robin' + delay_loop: 60 + forward_method: 'direct' + persistence_timeout: 30 + port: 443 + protocol: 'tcp' + real_server: + - address: '10.10.10.1' + connection_timeout: 61 + port: 443 + state: deleted + + # After state: + + # vyos@vyos:~$ show configuration commands | match "set high-availability" + # set high-availability disable + # set high-availability virtual-server s2 address '10.10.10.2' + # set high-availability virtual-server s2 persistence-timeout '30' + # set high-availability virtual-server s2 port '81' + # 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 group g1 peer-address '192.168.1.3' + # set high-availability vrrp group g1 priority '100' + # set high-availability vrrp group g1 vrid '20' + # set high-availability vrrp snmp + # set high-availability vrrp sync-group sg1 health-check failure-count '5' + + # vyos@vyos:~$ + # + # + # Module Execution: + # + # "after": { + # "disable": true, + # "virtual_servers": [ + # { + # "address": "10.10.10.2", + # "name": "s2", + # "persistence_timeout": 30, + # "port": 81, + # "protocol": "tcp" + # }, + # { + # "address": "10.10.10.3", + # "name": "s3", + # "port": 88, + # "protocol": "udp" + # } + # ], + # "vrrp": { + # "global_parameters": { + # "garp": { + # "master_repeat": 6 + # } + # }, + # "groups": [ + # { + # "disable": false, + # "name": "g1", + # "no_preempt": false, + # "peer_address": "192.168.1.3", + # "priority": 100, + # "rfc3768_compatibility": false, + # "vrid": 20 + # } + # ], + # "snmp": "enabled", + # "sync_groups": [ + # { + # "health_check": { + # "failure_count": 5 + # }, + # "name": "sg1" + # } + # ] + # } + # }, + # "before": { + # "disable": true, + # "virtual_servers": [ + # { + # "address": "10.10.10.5", + # "algorithm": "round-robin", + # "name": "s1", + # "real_server": [ + # { + # "address": "10.10.50.2", + # "port": 8443 + # } + # ] + # }, + # { + # "address": "10.10.10.2", + # "name": "s2", + # "persistence_timeout": 30, + # "port": 81, + # "protocol": "tcp" + # }, + # { + # "address": "10.10.10.3", + # "name": "s3", + # "port": 88, + # "protocol": "udp" + # } + # ], + # "vrrp": { + # "global_parameters": { + # "garp": { + # "master_repeat": 6 + # }, + # "startup_delay": 30 + # }, + # "groups": [ + # { + # "disable": false, + # "name": "g1", + # "no_preempt": false, + # "peer_address": "192.168.1.3", + # "priority": 100, + # "rfc3768_compatibility": false, + # "vrid": 20 + # } + # ], + # "snmp": "enabled", + # "sync_groups": [ + # { + # "health_check": { + # "failure_count": 5 + # }, + # "name": "sg1" + # } + # ] + # } + # }, + # "changed": true, + # "commands": [ + # "delete high-availability virtual-server s1", + # "delete high-availability vrrp global-parameters startup-delay" + # ], + + # Using purged: + + # Before state: + + # vyos@vyos:~$ show configuration commands | match "set high-availability" + # set high-availability disable + # set high-availability virtual-server s2 address '10.10.10.2' + # set high-availability virtual-server s2 persistence-timeout '30' + # set high-availability virtual-server s2 port '81' + # 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 group g1 peer-address '192.168.1.3' + # set high-availability vrrp group g1 priority '100' + # set high-availability vrrp group g1 vrid '20' + # set high-availability vrrp snmp + # set high-availability vrrp sync-group sg1 health-check failure-count '5' + # vyos@vyos:~$ + + + - name: Purge configuration + vyos.vyos.vyos_ha: + config: + state: purged + + # After state: + + # vyos@vyos:~$ show configuration commands | match "set high-availability" + # vyos@vyos:~$ + # + # Module Execution: + # + # "after": { + # "disable": false + # }, + # "before": { + # "disable": true, + # "virtual_servers": [ + # { + # "address": "10.10.10.2", + # "name": "s2", + # "persistence_timeout": 30, + # "port": 81, + # "protocol": "tcp" + # }, + # { + # "address": "10.10.10.3", + # "name": "s3", + # "port": 88, + # "protocol": "udp" + # } + # ], + # "vrrp": { + # "global_parameters": { + # "garp": { + # "master_repeat": 6 + # } + # }, + # "groups": [ + # { + # "disable": false, + # "name": "g1", + # "no_preempt": false, + # "peer_address": "192.168.1.3", + # "priority": 100, + # "rfc3768_compatibility": false, + # "vrid": 20 + # } + # ], + # "snmp": "enabled", + # "sync_groups": [ + # { + # "health_check": { + # "failure_count": 5 + # }, + # "name": "sg1" + # } + # ] + # } + # }, + # "changed": true, + # "commands": [ + # "delete high-availability" + # ], + + + # using gathered: + # -------------- + + # Before state: + # vyos@vyos:~$ + # show configuration commands | match "set high-availability" + # set high-availability disable + # set high-availability virtual-server s1 address '10.10.10.5' + # set high-availability virtual-server s1 algorithm 'round-robin' + # set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' + # set high-availability virtual-server s2 address '10.10.10.2' + # set high-availability virtual-server s2 persistence-timeout '30' + # set high-availability virtual-server s2 port '81' + # 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 '30' + # 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 vrid '20' + # set high-availability vrrp snmp + # set high-availability vrrp sync-group sg1 health-check failure-count '5' + # vyos@vyos:~$ + + - name: gather configs + vyos.vyos.vyos_ha: + state: gathered + + # Module Execution: + # "changed": false, + # "gathered": { + # "disable": true, + # "virtual_servers": [ + # { + # "address": "10.10.10.5", + # "algorithm": "round-robin", + # "name": "s1", + # "real_server": [ + # { + # "address": "10.10.50.2", + # "port": 8443 + # } + # ] + # }, + # { + # "address": "10.10.10.2", + # "name": "s2", + # "persistence_timeout": 30, + # "port": 81, + # "protocol": "tcp" + # }, + # { + # "address": "10.10.10.3", + # "name": "s3", + # "port": 88, + # "protocol": "udp" + # } + # ], + # "vrrp": { + # "global_parameters": { + # "garp": { + # "master_repeat": 6 + # }, + # "startup_delay": 30 + # }, + # "groups": [ + # { + # "disable": false, + # "name": "g1", + # "no_preempt": false, + # "peer_address": "192.168.1.3", + # "priority": 100, + # "rfc3768_compatibility": false, + # "vrid": 20 + # } + # ], + # "snmp": "enabled", + # "sync_groups": [ + # { + # "health_check": { + # "failure_count": 5 + # }, + # "name": "sg1" + # } + # ] + # } + # }, + # + + # Using parsed: + # ------------ + + # parsed.cfg + # set high-availability vrrp group g1 interface eth2 + # set high-availability vrrp group g1 address 1.1.1.1 + # set high-availability vrrp group g1 disable + # set high-availability vrrp group g1 no-preempt + # set high-availability vrrp group g1 advertise-interval 10 + # set high-availability vrrp group g1 peer-address 2.2.2.2 + # set high-availability vrrp group g1 rfc3768-compatibility + # set high-availability vrrp group g1 vrid 20 + + - name: parse configs + vyos.vyos.vyos_ha: + running_config: "{{ lookup('file', './parsed.cfg') }}" + state: parsed + + # Module execution: + # "parsed": { + # "disable": false, + # "vrrp": { + # "groups": [ + # { + # "address": "1.1.1.1", + # "advertise_interval": 10, + # "disable": true, + # "interface": "eth2", + # "name": "g1", + # "no_preempt": true, + # "peer_address": "2.2.2.2", + # "rfc3768_compatibility": true, + # "vrid": 20 + # } + # ] + # } + # } + # + + # Using rendered: + # -------------- + + - name: Render + vyos.vyos.vyos_ha: + config: + disable: true + vrrp: + snmp: enabled + global_parameters: + startup_delay: 32 + version: 3 + garp: + interval: 30 + master_delay: 11 + master_refresh: 100 + master_refresh_repeat: 200 + master_repeat: 5 + state: rendered + + # Module Execution: + # "rendered": [ + # "set high-availability disable", + # "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 5", + # "set high-availability vrrp global-parameters startup-delay 32", + # "set high-availability vrrp global-parameters version 3", + # "set high-availability vrrp snmp" + # ] + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ after + +
+ dictionary +
+
when changed +
The resulting configuration after module execution.
+
+
Sample:
+
This output will always be in the same format as the module argspec.
+
+
+ before + +
+ dictionary +
+
when state is merged, replaced, overridden, deleted or purged +
The configuration prior to the module execution.
+
+
Sample:
+
This output will always be in the same format as the module argspec.
+
+
+ commands + +
+ list +
+
when state is merged, replaced, overridden, deleted or purged +
The set of commands pushed to the remote device.
+
+
Sample:
+
["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 description 'Group 1'"]
+
+
+ gathered + +
+ list +
+
when state is gathered +
Facts about the network resource gathered from the remote device as structured data.
+
+
Sample:
+
This output will always be in the same format as the module argspec.
+
+
+ parsed + +
+ list +
+
when state is parsed +
The device native config provided in running_config option parsed into structured data as per module argspec.
+
+
Sample:
+
This output will always be in the same format as the module argspec.
+
+
+ rendered + +
+ list +
+
when state is rendered +
The provided configuration in the task rendered in device-native format (offline).
+
+
Sample:
+
["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'"]
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Evgeny Molotkov (@omnom62) diff --git a/meta/runtime.yml b/meta/runtime.yml index fe819fdf..9bee831f 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -68,3 +68,5 @@ plugin_routing: redirect: vyos.vyos.vyos_vlan vrf: redirect: vyos.vyos.vyos_vrf + ha: + redirect: vyos.vyos.vyos_ha diff --git a/plugins/action/ha.py b/plugins/action/ha.py new file mode 120000 index 00000000..331a791f --- /dev/null +++ b/plugins/action/ha.py @@ -0,0 +1 @@ +vyos.py \ No newline at end of file diff --git a/plugins/module_utils/network/vyos/argspec/ha/__init__.py b/plugins/module_utils/network/vyos/argspec/ha/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/network/vyos/argspec/ha/ha.py b/plugins/module_utils/network/vyos/argspec/ha/ha.py new file mode 100644 index 00000000..4fe431cd --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/ha/ha.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 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 + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the vyos_ha module +""" + + +class HaArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_ha module""" + + argument_spec = { + "config": { + "type": "dict", + "required": False, + "options": { + "disable": {"type": "bool", "default": False}, + "virtual_servers": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "address": {"type": "str"}, + "algorithm": {"type": "str"}, + "delay_loop": {"type": "int"}, + "forward_method": {"type": "str", "choices": ["direct", "nat"]}, + "fwmark": {"type": "int"}, + "persistence_timeout": {"type": "int"}, + "port": {"type": "int"}, + "protocol": {"type": "str", "choices": ["tcp", "udp"]}, + "real_server": { + "type": "list", + "elements": "dict", + "options": { + "address": {"type": "str", "required": True}, + "port": {"type": "int"}, + "connection_timeout": {"type": "int"}, + "health_check_script": {"type": "str"}, + }, + }, + }, + }, + "vrrp": { + "type": "dict", + "options": { + "global_parameters": { + "type": "dict", + "options": { + "garp": { + "type": "dict", + "options": { + "interval": {"type": "int"}, + "master_delay": {"type": "int"}, + "master_refresh": {"type": "int"}, + "master_refresh_repeat": {"type": "int"}, + "master_repeat": {"type": "int"}, + }, + }, + "startup_delay": {"type": "int"}, + "version": {"type": "str"}, + }, + }, + "groups": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "address": {"type": "list", "elements": "str"}, + "advertise_interval": {"type": "int"}, + "authentication": { + "type": "dict", + "options": { + "password": {"type": "str", "no_log": True}, + "type": {"type": "str"}, + }, + }, + "description": {"type": "str"}, + "disable": {"type": "bool", "default": False}, + "excluded_address": {"type": "list", "elements": "str"}, + "garp": { + "type": "dict", + "options": { + "interval": {"type": "int"}, + "master_delay": {"type": "int"}, + "master_refresh": {"type": "int"}, + "master_refresh_repeat": {"type": "int"}, + "master_repeat": {"type": "int"}, + }, + }, + "health_check": { + "type": "dict", + "options": { + "failure_count": {"type": "int"}, + "interval": {"type": "int"}, + "ping": {"type": "str"}, + "script": {"type": "str"}, + }, + }, + "hello_source_address": {"type": "str"}, + "interface": {"type": "str"}, + "no_preempt": {"type": "bool", "default": False}, + "peer_address": {"type": "str"}, + "preempt_delay": {"type": "int"}, + "priority": {"type": "int"}, + "rfc3768_compatibility": {"type": "bool", "default": False}, + "track": { + "type": "dict", + "options": { + "exclude_vrrp_interface": {"type": "bool"}, + "interface": {"type": "list", "elements": "str"}, + }, + }, + "transition_script": { + "type": "dict", + "options": { + "backup": {"type": "str"}, + "fault": {"type": "str"}, + "master": {"type": "str"}, + "stop": {"type": "str"}, + }, + }, + "vrid": {"type": "int", "required": False}, + }, + }, + "snmp": { + "type": "str", + "choices": ["disabled", "enabled"], + }, + "sync_groups": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "health_check": { + "type": "dict", + "options": { + "failure_count": {"type": "int"}, + "interval": {"type": "int"}, + "ping": {"type": "str"}, + "script": {"type": "str"}, + }, + }, + "member": {"type": "list", "elements": "str"}, + "transition_script": { + "type": "dict", + "options": { + "backup": {"type": "str"}, + "fault": {"type": "str"}, + "master": {"type": "str"}, + "stop": {"type": "str"}, + }, + }, + }, + }, + }, + }, + }, + }, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "purged", + "replaced", + "gathered", + "rendered", + "parsed", + "overridden", + ], + "default": "merged", + }, + "running_config": {"type": "str"}, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/config/ha/__init__.py b/plugins/module_utils/network/vyos/config/ha/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/network/vyos/config/ha/ha.py b/plugins/module_utils/network/vyos/config/ha/ha.py new file mode 100644 index 00000000..7fc96a35 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/ha/ha.py @@ -0,0 +1,705 @@ +# +# -*- 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 config file. +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to its desired end-state is +created. +""" + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_empties, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ha import ( + HaTemplate, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version + + +class Ha(ResourceModule): + """ + The vyos_ha config class + """ + + def __init__(self, module): + super(Ha, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="ha", + tmplt=HaTemplate(), + ) + self.parsers = [ + "disable", + ] + + self._validate_template() + + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.4"): + self._tmplt = HaTemplate() + else: + self._module.fail_json(msg="High Availability is not supported in this version of VyOS") + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + + if self.state not in ["parsed", "gathered", "purged"]: + self.generate_commands() + self.run_commands() + + if self.state == "purged": + wantd = {"disable": False} + haved = deepcopy(self.have) + if wantd != haved: + self.commands = ["delete high-availability"] + self.run_commands() + if "before" in self.result: + self._normalize_lists(self.result["before"]) + if "after" in self.result: + self._normalize_lists(self.result["after"]) + if "parsed" in self.result: + self._normalize_lists(self.result["parsed"]) + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = deepcopy(self.want) + haved = deepcopy(self.have) + for entry in wantd, haved: + self._list_to_named_dict(entry) + self._normalize_lists(entry) + + if self.state in ["deleted"]: + wantd, haved, p = self._prune_stubs(self._module.params.get("config", {}), haved) + + if self.state in ["overridden"]: + wo = deepcopy(wantd) + self._diff_w_h(wo, haved) + + haved_disable = haved.get("disable") + + for k1, v1 in wo.items(): + + if not isinstance(v1, dict): + continue + + for name, obj in v1.items(): + if isinstance(obj, dict) and not obj: + wi, hi, pi = self._prune_stubs({k1: {name: {}}}, haved) + haved = hi + + for k2, v2 in v1.items(): + if not isinstance(v2, dict): + continue + + for name, obj in v2.items(): + if isinstance(obj, dict) and not obj: + wi, hi, pi = self._prune_stubs({k1: {k2: {name: {}}}}, haved) + haved = hi + + if haved_disable is not None: + haved["disable"] = haved_disable + + keys = set(wantd) | set(haved) + + for k in keys: + + want = wantd.get(k, {}) + have = haved.get(k, {}) + + if k == "vrrp": + if self.state in ["merged"]: + want = combine(have, want, recursive=True, list_merge="append_rp") + self._compare_vrrp(want, have) + + if k == "virtual_servers": + if self.state in ["merged"]: + want = combine(have, want, recursive=True) + self._compare_vsrvs(want, have) + + if self.state in ["deleted"] and k == "disable": + want = have + if self.state in ["overridden"] and k == "disable" and not want: + want = False + if self.state in ["rendered"]: + have = None + + self.compare( + parsers=self.parsers, + want={k: want}, + have={k: have}, + ) + + self.commands = list(dict.fromkeys(self.commands)) + + def _compare_vsrvs(self, want, have): + """Compare virtual servers. + + Pre-index both want and have by (name, attribute) signature so that + each lookup is O(1) instead of O(n). Groups that are identical + between want and have are skipped entirely via an equality + short-circuit before leaf decomposition. + """ + vs_parsers = [ + "virtual_servers.address", + "virtual_servers.algorithm", + "virtual_servers.delay_loop", + "virtual_servers.forward_method", + "virtual_servers.persistence_timeout", + "virtual_servers.fwmark", + "virtual_servers.port", + "virtual_servers.protocol", + "virtual_servers.real_server.port", + "virtual_servers.real_server.health_check_script", + "virtual_servers.real_server.connection_timeout", + ] + + want_index = ( + {vs["name"]: vs for vs in want.values() if isinstance(vs, dict) and vs.get("name")} + if isinstance(want, dict) + else {} + ) + have_index = ( + {vs["name"]: vs for vs in have.values() if isinstance(vs, dict) and vs.get("name")} + if isinstance(have, dict) + else {} + ) + + all_names = set(want_index) | set(have_index) + + for name in all_names: + w = want_index.get(name, {}) + h = have_index.get(name, {}) + + if w == h and self.state not in ["rendered"]: + continue + + wlist = self._extract_named_leafs(w) if w else [] + hlist = self._extract_named_leafs(h) if h else [] + + if self.state == "rendered": + hlist = [] + + def _vsrv_sig(item): + if not isinstance(item, dict): + return None + iname = item.get("name") + if not iname: + return None + if "real_server" in item: + rs = item["real_server"] + if not isinstance(rs, dict) or "address" not in rs: + return None + addr = rs["address"] + for k in rs: + if k != "address": + return ("real_server", iname, addr, k) + return ("real_server", iname, addr, None) + for k in item: + if k != "name": + return ("attr", iname, k) + return None + + have_leaf_index = {} + for hdict in hlist: + sig = _vsrv_sig(hdict) + if sig is not None: + have_leaf_index[sig] = hdict + + want_leaf_index = {} + for wdict in wlist: + sig = _vsrv_sig(wdict) + if sig is not None: + want_leaf_index[sig] = wdict + + if self.state in ["replaced", "deleted"]: + for sig, hdict in have_leaf_index.items(): + wdict = want_leaf_index.get(sig, {}) + if self.state == "deleted" and wdict: + wdict = {} + elif not wdict: + hdict = {} + self.compare( + parsers=vs_parsers, + want={"virtual_servers": wdict}, + have={"virtual_servers": hdict}, + ) + + if self.state in ["merged", "replaced", "rendered", "overridden"]: + for sig, wdict in want_leaf_index.items(): + hdict = have_leaf_index.get(sig, {}) + self.compare( + parsers=vs_parsers, + want={"virtual_servers": wdict}, + have={"virtual_servers": hdict}, + ) + + def _compare_vrrp(self, want, have): + """Compare VRRP groups and sync-groups. + + Pre-index groups by name so matching is O(1). Groups that are + identical between want and have are skipped via equality + short-circuit before any leaf decomposition occurs — this is the + dominant performance win for large idempotent runs. + """ + vrrp_parsers = [ + "vrrp.snmp", + "vrrp.global_parameters", + "vrrp.global_parameters.garp", + "vrrp.groups", + "vrrp.groups.disable", + "vrrp.groups.no_preempt", + "vrrp.groups.rfc3768_compatibility", + "vrrp.groups.address", + "vrrp.groups.excluded_address", + "vrrp.groups.garp", + "vrrp.groups.authentication", + "vrrp.groups.transition_script", + "vrrp.groups.health_check", + "vrrp.groups.track.interface", + "vrrp.groups.track.exclude_vrrp_interface", + "vrrp.sync_groups.member", + "vrrp.sync_groups.transition_script", + "vrrp.sync_groups.health_check", + ] + + if ( + have.get("snmp") == "enabled" + and want.get("snmp") != "enabled" + and self.state not in ["deleted", "overridden"] + and (self.state != "merged" or "snmp" in want) + ): + self.commands.append("delete high-availability vrrp snmp") + + non_named = {k: v for k, v in (want or {}).items() if k not in ("groups", "sync_groups")} + non_named_have = { + k: v for k, v in (have or {}).items() if k not in ("groups", "sync_groups") + } + + hlist_non = self._extract_leaf_items(non_named_have) + wlist_non = self._extract_leaf_items(non_named) + + if self.state == "rendered": + hlist_non = [] + + have_non_index = {} + for hdict in hlist_non: + sig = self._vrrp_leaf_sig(hdict) + have_non_index[sig] = hdict + + want_non_index = {} + for wdict in wlist_non: + sig = self._vrrp_leaf_sig(wdict) + want_non_index[sig] = wdict + + if self.state in ["replaced", "deleted"]: + for sig, hdict in have_non_index.items(): + wdict = want_non_index.get(sig, {}) + if self.state == "deleted" and wdict: + wdict = {} + if self.state == "replaced" and wdict and wdict != hdict: + wdict = {} + elif not wdict: + hdict = {} + self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict}) + + if self.state in ["merged", "replaced", "rendered", "overridden"]: + for sig, wdict in want_non_index.items(): + hdict = have_non_index.get(sig, {}) + self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict}) + + for section in ("groups", "sync_groups"): + want_objs = (want or {}).get(section, {}) + have_objs = (have or {}).get(section, {}) + + if not isinstance(want_objs, dict): + want_objs = {} + if not isinstance(have_objs, dict): + have_objs = {} + + all_names = set(want_objs) | set(have_objs) + + for name in all_names: + w = want_objs.get(name, {}) + h = have_objs.get(name, {}) + + if w == h and self.state not in ["rendered"]: + continue + + wlist = self._extract_leaf_items({section: {name: w}}) if w else [] + hlist = self._extract_leaf_items({section: {name: h}}) if h else [] + + if self.state == "rendered": + hlist = [] + + have_leaf_index = {} + for hdict in hlist: + sig = self._vrrp_leaf_sig(hdict) + have_leaf_index[sig] = hdict + + want_leaf_index = {} + for wdict in wlist: + sig = self._vrrp_leaf_sig(wdict) + want_leaf_index[sig] = wdict + + if self.state in ["replaced", "deleted"]: + for sig, hdict in have_leaf_index.items(): + wdict = want_leaf_index.get(sig, {}) + if self.state == "deleted" and wdict: + wdict = {} + if self.state == "replaced" and wdict and wdict != hdict: + wdict = {} + elif not wdict: + hdict = {} + self.compare( + parsers=vrrp_parsers, + want={"vrrp": wdict}, + have={"vrrp": hdict}, + ) + + if self.state in ["merged", "replaced", "rendered", "overridden"]: + for sig, wdict in want_leaf_index.items(): + hdict = have_leaf_index.get(sig, {}) + self.compare( + parsers=vrrp_parsers, + want={"vrrp": wdict}, + have={"vrrp": hdict}, + ) + + def _vrrp_leaf_sig(self, item): + """Build a hashable signature for a VRRP leaf dict for O(1) indexing.""" + if not isinstance(item, dict) or not item: + return () + + container = next(iter(item)) + inner = item[container] + + sig = [container] + + if isinstance(inner, dict) and "name" in inner: + sig.append(("name", inner["name"])) + + if isinstance(inner, dict): + for k, v in inner.items(): + if k == "name": + continue + if not isinstance(v, dict): + sig.append(k) + break + sig.append(k) + for leaf in v: + sig.append(leaf) + break + break + + return tuple(sig) + + def _list_to_named_dict(self, data): + """Convert all named-object lists to name-keyed dicts in-place. + + Replaces the three separate _vrrp_groups_list_to_dict, + _vrrp_sync_groups_list_to_dict, and _virtual_servers_list_to_dict + methods with a single helper. Also normalises real_server lists + inside virtual servers. + """ + # VRRP groups and sync_groups + vrrp = data.get("vrrp", {}) + for key in ("groups", "sync_groups"): + items = vrrp.get(key) + if isinstance(items, list): + vrrp[key] = { + item["name"]: item + for item in items + if isinstance(item, dict) and item.get("name") + } + + # Virtual servers + vss = data.get("virtual_servers") + if isinstance(vss, list): + new_vss = {} + for vs in vss: + if not isinstance(vs, dict): + continue + name = vs.get("name") + if not name: + continue + rs = vs.get("real_server") + if isinstance(rs, list): + vs["real_server"] = { + item["address"]: item + for item in rs + if isinstance(item, dict) and item.get("address") + } + new_vss[name] = vs + data["virtual_servers"] = new_vss + elif isinstance(vss, dict): + for vs in vss.values(): + if not isinstance(vs, dict): + continue + rs = vs.get("real_server") + if isinstance(rs, list): + vs["real_server"] = { + item["address"]: item + for item in rs + if isinstance(item, dict) and item.get("address") + } + + return data + + def _extract_leaf_items(self, data, path=None, parent_name=None): + path = path or [] + results = [] + + if isinstance(data, dict): + current_name = data.get("name", parent_name) + + for k, v in data.items(): + if k == "name" or (k == "snmp" and v == "disabled"): + continue + results.extend(self._extract_leaf_items(v, path + [k], current_name)) + return results + + leaf_key = path[-1] + top_key = path[0] + + if top_key in ["groups", "sync_groups"]: + subkeys = path[2:] + else: + subkeys = path[1:] + + nested = {leaf_key: data} + + for p in reversed(subkeys[:-1]): + nested = {p: nested} + if parent_name: + out = {top_key: {"name": parent_name}} + out[top_key].update(nested) + else: + out = {top_key: nested} + + results.append(out) + return results + + def _normalize_lists(self, node): + """ + Recursively normalize all lists inside a dict or list. + All lists are sorted to ensure consistent ordering for comparison. + """ + if isinstance(node, dict): + for k, v in node.items(): + if isinstance(v, list): + if all(not isinstance(i, (dict, list)) for i in v): + node[k] = sorted(v) + else: + for item in v: + self._normalize_lists(item) + elif isinstance(v, dict): + self._normalize_lists(v) + elif isinstance(node, list): + for item in node: + self._normalize_lists(item) + + def _extract_named_leafs(self, data, parent_name=None, prefix_key=None): + results = [] + + if prefix_key == "real_server" and isinstance(data, dict): + for d, server_data in data.items(): + if not isinstance(server_data, dict): + continue + + address = server_data.get("address") + if not address: + continue + + for k, v in server_data.items(): + if k == "address": + continue + + results.append( + { + "name": parent_name, + "real_server": { + "address": address, + k: v, + }, + }, + ) + return results + + if isinstance(data, dict): + current_name = data.get("name", parent_name) + + for k, v in data.items(): + if k == "name": + continue + + results.extend( + self._extract_named_leafs(v, current_name, k), + ) + + return results + + return [ + { + "name": parent_name, + prefix_key: data, + }, + ] + + def _prune_stubs(self, w, h, path=""): + wc = {} + hc = self._remove_defaults(h) + + if not self._remove_defaults(w) and remove_empties(hc): + self.commands = ["delete high-availability"] + return {}, {}, path + + for k, wg in (self._remove_defaults(w) or {}).items(): + next_path = f"{path} {k}".strip() + stub = self._cli_path(next_path) + hg = remove_empties(hc).get(k) + + if hg is None: + continue + + if not isinstance(wg, (dict, list)): + self.commands.append(f"delete high-availability {stub}") + hc.pop(k, None) + wc.pop(k, None) + continue + + if not wg: + self.commands.append(f"delete high-availability {stub}") + hc.pop(k, None) + wc.pop(k, None) + continue + + if isinstance(wg, list) and isinstance(hg, dict): + for item in wg: + name = item.get("name") + if not name: + continue + + if name in hg: + self.commands.append( + f"delete high-availability {stub} {name}", + ) + + hg.pop(name, None) + + if hg: + hc[k] = hg + else: + hc.pop(k, None) + + if self._remove_defaults(wg): + wc[k] = wg + else: + wc.pop(k, None) + + continue + + if isinstance(wg, dict) and isinstance(hg, dict): + wi, hi, p = self._prune_stubs(wg, hg, next_path) + + if wi: + wc[k] = wi + + if hi: + hc[k] = hi + else: + hc.pop(k, None) + + return wc, hc, path + + def _remove_defaults(self, data): + """Strip None and False from config dicts, but preserve "disabled". + + False is the argspec default for boolean flags (disable, no_preempt, + rfc3768_compatibility) and carries no config intent — stripping it + prevents spurious `delete` commands for fields already at their + default state. + + "disabled" is an explicit user choice for snmp and must be preserved + so that _prune_stubs can act on it. The original code stripped it, + which made `snmp: disabled` invisible to the deleted-state logic. + """ + if isinstance(data, dict): + cleaned = {} + for k, v in data.items(): + if v is None or v is False: + continue + v = self._remove_defaults(v) + cleaned[k] = v + return cleaned + return data + + def _cli_path(self, path): + token_map = { + "groups": "group", + "sync_groups": "sync-group", + "virtual_servers": "virtual-server", + } + + parts = [] + for p in path.split(): + p = token_map.get(p, p) + parts.append(p.replace("_", "-")) + + return " ".join(parts) + + def _diff_w_h(self, w, h): + + NAMED_OBJECT_KEYS = { + "groups", + "sync_groups", + "virtual_servers", + "global_parameters", + } + + if not isinstance(w, dict) or not isinstance(h, dict): + return w + + for key in w.keys() & h.keys(): + wv = w[key] + hv = h[key] + + if key in NAMED_OBJECT_KEYS and isinstance(wv, dict) and isinstance(hv, dict): + for name in wv.keys() & hv.keys(): + if wv[name] != hv[name] and isinstance(wv[name], (dict, list)): + wv[name] = {} + elif wv[name] != hv[name]: + wv[name] = None + continue + self._diff_w_h(wv, hv) + return w diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py index 24c444dd..7f7bad8a 100644 --- a/plugins/module_utils/network/vyos/facts/facts.py +++ b/plugins/module_utils/network/vyos/facts/facts.py @@ -30,6 +30,9 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firew from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_rules.firewall_rules import ( Firewall_rulesFacts, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ha.ha import ( + HaFacts, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.hostname.hostname import ( HostnameFacts, ) @@ -94,6 +97,7 @@ FACT_RESOURCE_SUBSETS = dict( firewall_rules=Firewall_rulesFacts, firewall_global=Firewall_globalFacts, firewall_interfaces=Firewall_interfacesFacts, + ha=HaFacts, ospfv3=Ospfv3Facts, ospfv2=Ospfv2Facts, ospf_interfaces=Ospf_interfacesFacts, diff --git a/plugins/module_utils/network/vyos/facts/ha/__init__.py b/plugins/module_utils/network/vyos/facts/ha/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/network/vyos/facts/ha/ha.py b/plugins/module_utils/network/vyos/facts/ha/ha.py new file mode 100644 index 00000000..fc559783 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/ha/ha.py @@ -0,0 +1,174 @@ +# -*- 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): + """Classify config lines into per-object buckets for isolated parsing. + + Each bucket is parsed by a single HaTemplate instance so that facts + from different objects (groups, sync-groups, virtual-servers) never + bleed into each other. + + Keys are namespaced to avoid collisions between a VRRP group and a + sync-group that share the same name (e.g. both named "g1"). + An elif chain ensures each line lands in exactly one bucket. + """ + config_dict = {} + for config_line in data.splitlines(): + vrrp_disable = re.search(r"set high-availability disable", config_line) + vrrp_snmp = re.search(r"set high-availability vrrp snmp", config_line) + vrrp_gp = re.search( + r"set high-availability vrrp global-parameters (\S+).*", + config_line, + ) + vrrp_grp = re.search(r"set high-availability vrrp group (\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) + + if vrrp_disable: + config_dict.setdefault("disable", []).append(config_line) + elif vrrp_snmp: + config_dict.setdefault("vrrp", []).append(config_line) + elif vrrp_gp: + config_dict.setdefault("global_parameters", []).append(config_line) + elif vrrp_grp: + key = "vrrp_group_{0}".format(vrrp_grp.group(1)) + config_dict.setdefault(key, []).append(config_line) + elif vrrp_sg: + key = "vrrp_sg_{0}".format(vrrp_sg.group(1)) + config_dict.setdefault(key, []).append(config_line) + elif vrrp_vsrv: + config_dict.setdefault(vrrp_vsrv.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("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"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def normalize_config(self, config): + if not config: + return config + + if isinstance(config.get("virtual_servers"), dict): + config["virtual_servers"] = list(config["virtual_servers"].values()) + + 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()) + + 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/rm_templates/ha.py b/plugins/module_utils/network/vyos/rm_templates/ha.py new file mode 100644 index 00000000..300b14a7 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/ha.py @@ -0,0 +1,1011 @@ +# -*- 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 Ha parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_vsrvs(config_data): + config_data = config_data["virtual_servers"] + command = [] + + cmd = "high-availability virtual-server {name}".format(**config_data) + for key, value in config_data.items(): + if key == "name" or isinstance(value, dict) or value is None: + continue + else: + command.append(f"{cmd} {key.replace('_', '-')} {value}") + + return command + + +def _tmplt_vsrvs_rsrv(config_data): + config_data = config_data["virtual_servers"] + command = [] + cmd = "high-availability virtual-server {name}".format(**config_data) + config_data = config_data["real_server"] + address = config_data["address"] + for key, value in config_data.items(): + if key == "address" or value is None: + continue + if value is not None and key == "health_check_script": + command.append(cmd + " real-server " + address + " health-check script " + value) + else: + command.append(cmd + " real-server " + f"{address} {key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_sgroup_hc(config_data): + config_data = config_data["vrrp"]["sync_groups"] + command = [] + cmd = "high-availability vrrp sync-group {name}".format(**config_data) + config_data = config_data["health_check"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " health-check " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_sgroup_ts(config_data): + config_data = config_data["vrrp"]["sync_groups"] + command = [] + cmd = "high-availability vrrp sync-group {name}".format(**config_data) + config_data = config_data["transition_script"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " transition-script " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_gp(config_data): + config_data = config_data["vrrp"]["global_parameters"] + command = [] + + cmd = "high-availability vrrp global-parameters".format(**config_data) + for key, value in config_data.items(): + if isinstance(value, dict) or value is None: + continue + else: + command.append(f"{cmd} {key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_gp_garp(config_data): + config_data = config_data["vrrp"]["global_parameters"]["garp"] + command = [] + cmd = "high-availability vrrp global-parameters garp" + + for key, value in config_data.items(): + if value is None: + continue + command.append(f"{cmd} {key.replace('_', '-')} {value}") + + return command + + +def _tmplt_vrrp_group(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + + for key, value in config_data.items(): + if ( + key == "name" + or isinstance(value, dict) + or isinstance(value, list) + or isinstance(value, bool) + or value is None + ): + continue + else: + if key == "description": + value = f"'{value}'" + command.append(f"{cmd} {key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_bool(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + + for key, value in config_data.items(): + if key != "name" and value is not None: + command.append(f"{cmd} {key.replace('_', '-')}") + return command + + +def _tmplt_vrrp_group_garp(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["garp"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " garp " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_auth(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["authentication"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " authentication " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_ts(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["transition_script"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " transition-script " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_sgroup_member(config_data): + sgroup = config_data["vrrp"]["sync_groups"] + command = [] + cmd = "high-availability vrrp sync-group {name}".format(**sgroup) + members = sgroup.get("member", []) + for member in members: + if member is None: + continue + command.append(f"{cmd} member {member}") + return command + + +def _tmplt_vrrp_group_exaddress(config_data): + group = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**group) + exaddresses = group.get("excluded_address", []) + for exaddress in exaddresses: + if exaddress is None: + continue + command.append(f"{cmd} excluded-address {exaddress}") + return command + + +def _tmplt_vrrp_group_address(config_data): + group = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**group) + addresses = group.get("address", []) + for address in addresses: + if address is None: + continue + command.append(f"{cmd} address {address}") + return command + + +def _tmplt_vrrp_group_hc(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["health_check"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " health-check " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_track_list(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["track"] + for key, value in config_data.items(): + if isinstance(value, list) and value is not None and key != "name": + for item in value: + command.append(cmd + " track " + f"{key.replace('_', '-')} {item}") + return command + + +def _tmplt_vrrp_group_track_bool(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["track"] + for key, value in config_data.items(): + if key != "name" and value is not None: + command.append(cmd + " track " + f"{key.replace('_', '-')}") + return command + + +class HaTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(HaTemplate, self).__init__( + lines=lines, + tmplt=self, + prefix=prefix, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "disable", + "getval": re.compile( + r""" + ^set + \shigh-availability + \s(?Pdisable) + $""", + re.VERBOSE, + ), + "setval": "high-availability disable", + "result": { + "disable": "{{ True if disable is defined else False }}", + }, + }, + { + "name": "virtual_servers.address", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + (?:\s+address\s+(?P
\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "address": "{{ address if address is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.algorithm", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + (?:\s+algorithm\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "algorithm": "{{ algorithm if algorithm is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.delay_loop", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + (?:\s+delay-loop\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "delay_loop": "{{ delay_loop if delay_loop is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.forward_method", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + (?:\s+forward-method\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "forward_method": "{{ forward_method if forward_method is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.fwmark", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + (?:\s+fwmark\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "fwmark": "{{ fwmark if fwmark is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.persistence_timeout", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + (?:\s+persistence-timeout\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "persistence_timeout": "{{ persistence_timeout if persistence_timeout is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.port", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + (?:\s+port\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "port": "{{ port if port is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.protocol", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + (?:\s+protocol\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "protocol": "{{ protocol if protocol is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.real_server.port", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + \sreal-server + \s+(?P
\S+) + (?:\s+port\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs_rsrv, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "real_server": { + "{{ address }}": { + "address": "{{ address }}", + "port": "{{ port if port is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "virtual_servers.real_server.health_check_script", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + \sreal-server + \s+(?P
\S+) + (?:\s+health-check\sscript\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs_rsrv, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "real_server": { + "{{ address }}": { + "address": "{{ address }}", + "health_check_script": "{{ hcscript if hcscript is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "virtual_servers.real_server.connection_timeout", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P\S+) + \sreal-server + \s+(?P
\S+) + (?:\s+connection-timeout\s+(?P\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs_rsrv, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "real_server": { + "{{ address }}": { + "address": "{{ address }}", + "connection_timeout": "{{ cont if cont is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.sync_groups.member", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\ssync-group + \s+(?P\S+) + \smember + \s+(?P\S+) + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_sgroup_member, + "result": { + "vrrp": { + "sync_groups": { + "{{ sgname }}": { + "name": "{{ sgname }}", + "member": [ + "{{ member }}", + ], + }, + }, + }, + }, + }, + { + "name": "vrrp.sync_groups.health_check", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\ssync-group + \s+(?P\S+) + \shealth-check + (?:\s+failure-count\s+(?P\S+))? + (?:\s+interval\s+(?P\S+))? + (?:\s+ping\s+(?P\S+))? + (?:\s+script\s+(?P