Skip to main content
RunBook Academy

VyOSLIV · API and AutomationAutomation

VyOS Ansible integration — vyos.vyos collection over network_cli, modules, idempotent playbooks

Advanced⏱ ~28 minansibleansible-galaxyansible-playbookvyosansible.netcommon.network_clivyos.vyos.vyos_configvyos.vyos.vyos_command

What you'll learn

  • Install the vyos.vyos collection and configure the Ansible inventory for the network_cli connection
  • Write a vyos_config task that declaratively configures the router
  • Write a vyos_command task that retrieves operational state
  • Recognise idempotency and use it to make the playbook safe to re-run

Prerequisites

Verified against VyOS 1.5.x LTS (circinus) · VyOS 1.4.x (sagitta) — legacy · FRRouting 10.x (VyOS 1.5) · Linux kernel 6.6 LTS (VyOS 1.5 base) · strongSwan 5.9.x (IPsec) · WireGuard 1.0.x (kernel module + userspace tooling) · 2026-08-18

Not yet marked complete on this device.

Ansible drives VyOS through the vyos.vyos collection. The collection does not talk to the router’s HTTP API. It opens an SSH session to the router’s CLI and drives the same configure / set / delete / commit sequence an operator would type by hand. The playbook is the operator’s declarative description of the desired router state; the transport underneath is plain SSH.

On VyOS 1.5 LTS, the vyos.vyos collection includes the vyos_config module (declarative configuration), the vyos_command module (operational commands), a vyos_facts module, and a family of resource modules for interfaces, BGP, OSPF, firewall rules, route maps and prefix lists. The collection is the foundation for the configuration-as-code pipeline and the change-management workflow.

This lesson covers the collection installation, the connection variables that actually work, the two core modules, and the production patterns for idempotent playbooks.

There is no httpapi connection for VyOS

The vyos.vyos collection

The collection is published on Ansible Galaxy:

Configuration changecontrol node
$ ansible-galaxy collection install vyos.vyos

What has to be true for it to work:

  • On the control node — a supported ansible-core. The collection’s README records testing against ansible-core 2.15 and later. The collection depends on ansible.netcommon, which ansible-galaxy pulls in automatically. The VyOS documentation additionally calls for the Paramiko Python module, packaged on Debian and Ubuntu as python3-paramiko; network_cli uses it as its default SSH implementation.
  • On the router — the SSH service enabled, and a login account Ansible can authenticate as. Nothing is installed on the router: network_cli drives the CLI, so no Python interpreter and no agent is needed on the VyOS side.

The collection’s modules record testing against VyOS 1.3.8, 1.4.2, 1.5, and the spring-2025 rolling release.

flowchart LR
  PB["Playbook task<br/>vyos_config or vyos_command"] --> CP["cliconf plugin<br/>vyos.vyos.vyos"]
  CP --> NC["Connection plugin<br/>ansible.netcommon.network_cli"]
  NC -->|"SSH port 22"| SSHD["VyOS sshd"]
  SSHD --> CLI["Interactive VyOS CLI session"]
  CLI --> CMD["configure then set and delete then commit"]

The module never touches the router directly. It hands the commands to the cliconf plugin, the cliconf plugin knows how to enter configuration mode and how to read the VyOS prompt back, and network_cli carries the bytes over SSH. Every command in the playbook is a command that would work if you typed it at the router’s prompt.

The router side

The router needs SSH reachable from the control node and an account for the automation to use. Public-key authentication is the production choice:

configure
set service ssh port '22'
set system login user automation authentication public-keys ansible-control type 'ssh-ed25519'
set system login user automation authentication public-keys ansible-control key 'AAAAC3NzaC1lZDI1NTE5AAAAI...'
commit
save
exit

The ansible-control string is an arbitrary identifier for the key, not a filename; VyOS uses it to let you hold several keys for the same user and delete them individually. The key value is the base64 body of the public key on its own: the ssh-ed25519 prefix goes in the type node and the trailing comment is dropped.

Inventory and credentials

The inventory maps hostname to connection variables:

# inventory/hosts.yml
all:
  vars:
    ansible_connection: ansible.netcommon.network_cli
    ansible_network_os: vyos.vyos.vyos
    ansible_user: automation
    ansible_ssh_private_key_file: ~/.ssh/vyos_automation

  children:
    edge:
      hosts:
        edge-01:
          ansible_host: 10.99.0.1
        edge-02:
          ansible_host: 10.99.0.2

The variables specify:

  • Connectionansible.netcommon.network_cli opens a persistent SSH session to the router and drives the CLI through it.
  • Network OSvyos.vyos.vyos selects the VyOS cliconf and terminal plugins, so network_cli knows what the prompt looks like and how to enter configuration mode.
  • Credentialsansible_user is the VyOS login name. ansible_ssh_private_key_file points at the matching private key. If you authenticate with a password instead of a key, set ansible_password and source it from Ansible Vault:
    ansible_user: automation
    ansible_password: "{{ vault_vyos_password }}"

If the routers sit behind a bastion, network_cli takes the jump host through the Paramiko proxy variable documented on the platform-options page:

    ansible_paramiko_proxy_command: '-o ProxyCommand="ssh -W %h:%p -q bastion01"'

That page also warns that passwords cannot be passed through ProxyCommand, precisely so a secret does not end up on a process command line. Use a key for the bastion hop.

The defensive idiom: credentials live in Ansible Vault or in an SSH key that never leaves the control node, not in plaintext in the inventory file. The inventory file is committed to version control; the vault file and the private key are not.

vyos_config — declarative configuration

The vyos_config module applies a set of configuration commands to the router and commits them. Its lines argument takes the literal VyOS set and delete commands:

# playbook.yml
- name: Configure BGP on edge routers
  hosts: edge
  gather_facts: false
  tasks:
    - name: Configure BGP ASN and neighbour
      vyos.vyos.vyos_config:
        lines:
          - set protocols bgp system-as 64512
          - set protocols bgp parameters router-id 10.99.0.1
          - set protocols bgp neighbor 10.0.0.1 remote-as 65001
          - set protocols bgp neighbor 10.0.0.1 password '{{ vault_bgp_password }}'
        comment: 'ansible: bgp baseline'
        backup: true

The options that matter in production, with their documented defaults:

  • lines — the ordered set of commands. They must be the exact commands as they appear in the device’s running configuration, which is what makes the comparison in the next section work.
  • src — a path to a source config file instead of inline lines. The file may be in bracket format or set format and may contain Jinja2 template variables.
  • matchline by default, meaning the desired config is compared against the active config and only the deltas are loaded. none ignores the active configuration and always loads every line.
  • backupfalse by default. When true, the module copies the device’s active configuration to the Ansible control host before making any change, into a backup directory beside the playbook unless backup_options says otherwise.
  • comment — the commit description, defaulting to configured by vyos_config. It shows up in the router’s commit log, which is the difference between an audit trail that names the change and one that does not.
  • savefalse by default. Saving is independent of committing: the module commits whatever changes it sends, and save: true additionally writes the active configuration to disk so it survives a reboot. A playbook that commits without saving leaves a router that reverts on its next reload.
  • confirm and confirm_timeout — covered below.
  • allow_password_changeplaintext by default, which permits plaintext password changes and filters out encrypted-password keys. Set it to all, encrypted or none to change which password lines survive.

The src form is what the configuration-as-code pipeline uses:

- name: Render a Jinja2 template onto the VyOS router
  vyos.vyos.vyos_config:
    src: vyos_template.j2

vyos_command — operational commands

The vyos_command module runs operational commands and returns their output. It is read-only: it does not enter configuration mode and does not modify the router.

- name: Retrieve BGP summary
  vyos.vyos.vyos_command:
    commands:
      - show ip bgp summary
  register: bgp_summary

- name: Print BGP summary
  ansible.builtin.debug:
    var: bgp_summary.stdout_lines

The module returns stdout (the set of responses, one per command), stdout_lines (each response split into a list), failed_conditions and warnings.

The module also polls, which is what makes it useful for validating a change that takes time to converge:

  • wait_for — conditionals evaluated against the output.
  • matchall by default; any passes when a single conditional is satisfied.
  • retries — how many attempts before the command is considered failed. Default 9.
  • interval — seconds between retries. Default 1.
- name: Wait for the BGP session to reach Established
  vyos.vyos.vyos_command:
    commands:
      - show ip bgp summary
    wait_for:
      - "result[0] contains Established"
    retries: 30
    interval: 5

That task retries for up to 150 seconds and fails the play if the session never comes up, which is exactly the post-change gate a production playbook needs.

Commit-confirm from a playbook

The most dangerous change is one that severs the SSH session Ansible is using — a firewall rule, an interface address, a routing change on the management path. The module has the VyOS commit-confirm mechanism built in:

- name: Push the firewall baseline, revert if we lose the session
  vyos.vyos.vyos_config:
    src: firewall.j2
    confirm: automatic
    confirm_timeout: 10

confirm takes automatic, manual, or none, and defaults to none. With automatic, the module confirms the configuration itself, but only if the current session is still working with the new config; if the change locked the session out, the confirmation never arrives and the router reverts. With manual, the module leaves the confirmation to you. confirm_timeout is the number of minutes the router waits before reverting, defaulting to 10.

This is the single most valuable option in the module for remote work, and it is off by default.

Idempotency — the production safety net

Idempotency is the property that running an operation several times has the same effect as running it once. On VyOS the underlying commands help: set protocols ospf area 0 network 10.0.0.0/24 is a tree assignment, not an append, so re-issuing it at the CLI does not create a second entry. What repetition costs you is a playbook that can never tell you whether anything changed.

# Reports changed: true on every run
- name: Configure an interface description
  vyos.vyos.vyos_config:
    lines:
      - set int eth eth2 description 'OUTSIDE'
# Reports changed: false once the router already matches
- name: Configure an interface description
  vyos.vyos.vyos_config:
    lines:
      - set interfaces ethernet eth2 description 'OUTSIDE'

Both tasks leave the router in the same state. Only the second one converges: after the first successful run it reports changed: false, it stops entering configuration mode, and it stops writing a commit into the router’s commit log. The first one commits a no-op every run, which buries the one commit that mattered under a hundred that did not, and makes changed: true worthless as a signal.

The test is mechanical and takes thirty seconds: run the playbook twice. If the second run is not changed: false for every task, the playbook is not idempotent yet.

Failure modes

SSH authentication fails

The control node cannot log in — wrong user, key not installed on the router, or the router’s host key changed and the control node refuses it.

Diagnostic:

fatal: [edge-01]: UNREACHABLE! => {"changed": false, "msg": "<the SSH error, verbatim from the transport>", "unreachable": true}

Fix: verify the login by hand first — ssh automation@10.99.0.1 from the control node. An Ansible connection problem is almost always an SSH problem you can reproduce without Ansible. If the router was rebuilt, its host key changed; refresh the control node’s known_hosts deliberately rather than disabling host-key checking globally.

Commit fails

The module applies the configuration and commits. If the commit fails — the VyOS validator catches a typo, or a referenced object does not exist — the module reports an error, and the router’s active configuration is unchanged.

Diagnostic:

TASK [Configure BGP ASN and neighbour] ****************
fatal: [edge-01]: FAILED! => {"changed": false, "msg": "<the commit error the router printed>"}

Fix: correct the configuration and re-run. Because the commit is atomic, there is no half-applied state to clean up.

Every run reports changed

The playbook is green but never converges: run 1, run 2 and run 50 all report changed: true on the same task.

Diagnostic: run with --diff and read what the module proposes to send. If it is proposing lines the router already has, the lines are not in the router’s own text form.

Fix: run show configuration commands | match ethernet on the router, copy the line the router prints, and use that text in the playbook. Check that the task is not using match: none.

The change locks the playbook out

A firewall or interface change severs the SSH session the module is running over. The task hangs, then fails, and the router is left holding a configuration nobody can reach.

Diagnostic: the task times out; the router is unreachable afterwards.

Fix: confirm: automatic with a confirm_timeout on every task that touches the management path. The router reverts on its own when the confirmation does not arrive. Recovery without it means console access.

Inventory mismatch

The inventory lists edge-01 at 10.99.0.1, but the router is at 10.99.0.10.

Diagnostic:

fatal: [edge-01]: UNREACHABLE! => {"changed": false, "msg": "<connection timed out to 10.99.0.1>", "unreachable": true}

Fix: correct the inventory. The defensive idiom: use DNS names in the inventory, not IP addresses, so the address of record lives in one place.

Rollback

An Ansible-driven change is reversible through the standard VyOS mechanisms and through the module itself:

  • Before the changebackup: true writes the router’s pre-change active configuration to the control node. That file is the input to the rollback.
  • During the changeconfirm: automatic with confirm_timeout makes the router revert by itself if the session dies.
  • After the changerollback N then commit on the router reverts to a previous configuration revision, the same as any hand-made change.

The VyOS commit validator applies to a rollback configuration exactly as it applies to a forward change, so a rollback playbook fails loudly rather than half- applying.

Production discipline

Cross-course references

  • LIV-VyOS-Automation (vyos-liv-01-vyos-http-api, the previous lesson) covers the VyOS HTTP API. It is a parallel automation surface for tools you write yourself; the vyos.vyos collection does not use it.
  • LIV-VyOS-Automation (vyos-liv-03-config-as-code) covers the pipeline that renders the templates vyos_config’s src argument consumes.
  • LIV-VyOS-Automation (vyos-liv-04-automated-validation) covers the validation patterns that complement these playbooks.
  • VI-VyOS-CommitRollback (vyos-vi-02-commit-confirm) covers the commit-confirm mechanism that the module’s confirm option drives.
  • The Ansible course’s IV-Ansible-Inventory covers inventory structure, and XII-Ansible-Idempotency covers the idempotency model the changed flag reports against.

Quiz

Knowledge check · 4 questions

  1. Q1. Which Ansible connection does the vyos.vyos collection use to reach a VyOS router?

  2. Q2. By default `vyos_config` compares the lines you supply against the router's active configuration and sends only the difference; `match: none` turns that comparison off and sends every line on every run.

  3. Q3. A playbook task sets an interface description with `vyos.vyos.vyos_config` and `lines: - set int eth eth2 description 'OUTSIDE'`. The router has the description configured correctly, yet every scheduled run reports `changed: true` and writes another commit into the router's commit log. The team has stopped trusting the `changed` flag. What is happening and how do you fix it?

    A nightly Ansible run against two VyOS edge routers. One vyos_config task reports changed: true on every run for months. `show configuration commands | match eth2` on the router shows the description is already correct.

  4. Q4. An operator pushes a firewall baseline to a remote VyOS router with `vyos_config` and `src: firewall.j2`. The rendered rules do not permit SSH from the automation network. The task hangs and then fails, and the router is now unreachable from both the control node and the operator's laptop. What should have been in the task, and what are the options now?

    A remote VyOS edge router, no console server, reachable only over SSH from the automation network. An Ansible firewall push has just cut that path.

Passing score: 75%. Answers are checked in this browser.