View Categories

Getting Started with Ansible and Versa

by Akos Csiszer

Introduction

Ansible is an open-source automation tool used for configuration management, application deployment, and task automation. It allows IT administrators and DevOps teams to manage complex systems more efficiently and consistently. The development of Ansible started in February 2012.

Unlike traditional tools, Ansible uses a simple, human-readable language (YAML) to describe automation jobs in Playbooks. It operates in an agentless manner, connecting to managed nodes via SSH or REST API, which reduces setup overhead and simplifies maintenance.

Key benefits of Ansible include:

• Simplicity: Easy-to-read syntax and minimal learning curve.

• Agentless Architecture: No software needs to be installed on managed systems.

• Idempotency: Ensures that tasks only make changes when necessary, making automation predictable and repeatable.

• Scalability: Suitable for managing a few servers or thousands of systems.



Ansible is widely adopted in DevOps practices and is ideal for automating repetitive tasks, ensuring consistent configurations, and speeding up environment changes meanwhile keeping a low failure rate.

This document demonstrates the use of Ansible with Versa Director through an example.

Ansible key concepts

This document does not aim to provide a comprehensive overview of Ansible. However, to understand the provided example, the following key concepts should be explained:

• Playbook: A playbook is a YAML file that defines automation tasks. It consists of one or more plays, and each play specifies the tasks to be executed on a group of hosts — in our case, devices.

• Inventory: The inventory is a file that lists the target hosts where automation tasks will run. Depending on the environment, these hosts may be actual device names or template names. Hosts can be organized into groups, and a single host can belong to multiple groups.

• Variables: Variables define specific attributes of the hosts. They can be assigned at the group or host level. If a variable with the same name is defined in multiple places, the one closest to the host level takes precedence.


For more information and tutorials about Ansible, please visit the Ansible doc page.

Example: Using Versa Director REST API

Let’s consider a simple SD-WAN deployment consisting of a single Director, a single Controller, and three branches. One branch, called SmallBranch, is connected to the internet through a single connection. The medium-sized branch features a dual connection, configured in an active-standby setup.

NOTE: the Versa Director FQDN: director.my-lab.versa doesn’t exist and used just for this document as a placeholder for director FQN.

Figure 1. Example topology

The task is to update an existing address object named “Test-Address-Object” with different values based on branch groupings:

• For the group of branches called “mediumbranches“, the value should be 1.1.1.1/32.

• For one specific device within that group, MediumBranch-1-A, the value should be overridden to 2.2.2.2/32.

• For the “smallbranches” group, the value should be 3.3.3.3/32.

For simplicity and demonstration purposes, this configuration will be applied in the device context, rather than at the template level.

NOTE: All components in the topology were using the 1st May 2025 service release of 22.1.4, however the used API is also available in earlier versions.

NOTE: All files in the document are available in the Versa Networks Gitlab repostitory.

The inventory

With Ansible, it’s always a good practice to start by building the inventory. A well-structured inventory can be reused across multiple playbooks, making your automation more efficient and manageable. In this example, the following inventory file is used:

Filename: inventory.yaml
---
all:
  children:
    controllers:
      hosts:
        Controller-1:
    smallbranches:
      hosts:
        SmallBranch-1:
    mediumbranches:
      hosts:
        MediumBranch-1-A:
        MediumBranch-2-S:
    branches:
      children:
        mediumbranches:
        smallbranches:
This inventory defines the following groups:

• all: A default group that includes all hosts and sub-groups.

• controllers: Contains a single host named Controller-1.

• smallbranches: Contains one host named SmallBranch-1

• mediumbranches: Contains the redundant MediumBranch devices

• branches: A parent group that includes both the smallbranches and mediumbranches groups.

The variables The task requires setting different values for an address object based on the branch type. We’ll define a variable named address, with the following values:

• Set to 1.1.1.1/32 for the mediumbranches group at the group level.

• Set to 2.2.2.2/32 for the MediumBranch-1-A host at the host level.

• Set to 3.3.3.3/32 for the smallbranches group at the group level.

Note: Ansible processes group-level variables first, followed by host-level variables. Therefore, for MediumBranch-1-A, the host-level value 2.2.2.2/32 overrides the group-level value 1.1.1.1/32.

Variable Definitions

Group-level variables should be stored in the group_vars directory, while host-level variables belong in the host_vars directory. The filename in the directory refers to either the group or hostname.

Filename: group_vars/mediumbranches.yaml
---
address: "1.1.1.1/32"
Filename: host_vars/MediumBranch-1-A.yaml
---
address: "2.2.2.2/32"
Filename: group_vars/smallbracnhes.yaml
---
address: "3.3.3.3/32"
We could have defined the address object at the host level, since there is only one host in this group. However, the task explicitly requires it to be defined at the group level. This approach also ensures that any new hosts added to the group in the future will automatically inherit the correct value.

Filename: group_vars/all.yaml

---
versa_director: "https:// director.my-lab.versa:9182"
orgName: "Provider"
addressObjectName: "Test-Address-Object"

Since all hosts are part of the “all” group, any variables defined at this level will be available to all hosts. Defining shared variables here is good practice, as it improves the portability and reusability of the playbook.

In this file the following variables are defined:

• Versa Director base URL is defined in the versa_director variable. The Director API is accessible on both TCP ports 9182 and 9183. Port 9182 uses Basic Authentication, while Port 9183 uses OAuth (token-based) Authentication. For more information about the authentication methods visit the Versa Director REST API documentation.

• The variable orgName stores the name of the organization where the target address object is defined.

• The addressObjectName is the actual name of the address object to be updated.

The playbook

The playbook has a task defined to set the address object using the defined variable. For demonstration purposes, two additional tasks have been added: one retrieves the value of the object, and the other prints it on the screen.

Before executing the playbook, Ansible prompts the user to enter the Versa Director username and password. In production environments, it is best practice to use a vault system to securely store and retrieve credentials, rather than entering them manually.

The complete playbook can be found in the Versa Networks Gitlab repostitory.

Let’s look at the playbook block by block:

---
- name: "Update Address Object"
  hosts: branches
  connection: local
  gather_facts: false
  vars_prompt:
    - name: username
      prompt: "Director username: "
      private: false
    - name: password
      prompt: "Director password: "
      private: true
  tasks:

This section performs the following functions:

• Targeting hosts: The hosts key specifies that the tasks in this play should run on the hosts in the “branches” group. In our case, this group includes the branch devices but excludes the controller.

• Connection method: By default, Ansible executes tasks on the target hosts—in this case, the branch devices. However, the connection: local directive overrides this behavior and ensures that tasks are executed on the local machine where Ansible is running. This is required because the Versa Director REST API is called from the local machine, hence no need to connect to each target device.

• Fact gathering: When gather_facts is enabled, Ansible connects to all target hosts before running any tasks to collect system information such as the operating system type, Python version, and more.

• User input prompt: The vars_prompt section instructs Ansible to prompt the user for the username and password at runtime. The password input is masked to ensure it is not displayed during entry.

• Task definitions: After the tasks key the tasks for the play are defined.

Let’s look at the first task that sets the address object:
    - name: "Update Address Object"
      ansible.builtin.uri:
        url: "{{ versa_director }}/api/config/devices/device/{{ inventory_hostname }}/config/orgs/org-services/{{ orgName }}/objects/addresses/address/%22{{ addressObjectName }}%22"
        user: "{{ username  }}"
        password: "{{ password }}"
        method: "PUT"
        status_code: 204
        headers:
          Accept: "application/json"
        body_format: json
        body:
          address:
            name: "{{ addressObjectName }}"
            ipv4-prefix: "{{ address }}"
        validate_certs: false
        force_basic_auth: true

This task uses Ansible’s built-in uri module, which is designed for interacting with REST APIs. In this example, the module sends an HTTP PUT request to a specified URL, along with custom headers and a body payload. It uses HTTP Basic Authentication with the provided username and password and expects a response with status code 204 (No Content).

The URL is constructed dynamically for each device using the following variables:

• versa_director: Defined in group_vars/all.yaml, this variable contains the base URL of the Versa Director, including the port number.

• inventory_hostname: A built-in Ansible variable that holds the name of the host on which the task is being executed (in our case, the name of the branch device).

• orgName: Defined also in group_vars/all.yaml, this variable holds the name of the organization (e.g., Provider in this example).

• addressObjectName: Defined also in group_vars/all.yaml, this contains the name of the address object (e.g., Test-Address-Object in this example).

There are two main ways to discover the necessary API endpoints and payload structures:

1. Swagger Documentation: The REST API documentation is accessible via the Versa Director UI. After logging in, click on your username in the top right corner and select “Rest API Document”.

2. Browser Developer Tools: In practice, it’s often easier to perform the action manually in the Versa Director UI while using the browser’s developer tools to inspect network activity. This allows you to capture the exact URLs and payloads used.

The final two tasks in the playbook retrieve the address object by making an API call using the GET method and then displaying the result on the screen.

    - name: "GET Address Object"
      ansible.builtin.uri:
        url: "{{ versa_director }}/api/config/devices/device/{{ inventory_hostname }}/config/orgs/org-services/{{ orgName }}/objects/addresses?deep"
        user: "{{ username  }}"
        password: "{{ password }}"
        method: "GET"
        status_code: 200
        return_content: true
        headers:
          Accept: "application/json"
        validate_certs: false
        force_basic_auth: true
      register: configuredAddress
– ansible.builtin.debug: msg: “Address Object details: {{ configuredAddress .json }}”

To store the return value of the REST API call, the register option is used. In the example above, the output is stored in a variable named configuredAddress.

The final task uses Ansible’s built-in debug module to display a message along with the value of the configuredAddress variable on the screen. This use of the debug module is primarily for demonstration purposes. While it’s very helpful during playbook development for inspecting variable values, it is typically removed in production environments.

How to run

In the previous sections, we discussed the various building blocks of an Ansible playbook. This section focuses on how to execute the playbook.

Below is a screenshot showing the playbook execution. The playbook is executed using the following command:

ansible-playbook -i inventory.yaml playbooks/updateAddressObject.yaml
The -i option specifies the inventory file to be used—in this case, inventory.yaml. Here is an example run output:

Ansible reads the inventory file and executes the play on all defined hosts. In our example, the target group is branches, which includes three branch devices. Each task is executed in parallel across all devices. However, Ansible only proceeds to the next task once the current task has been completed on all hosts.

At the end of the run, Ansible provides a summary of the execution. In this case, all three tasks are completed successfully on each device.

Note that idempotency—a key feature of Ansible—was not applied in this example. The uri module is not inherently idempotent because its behavior depends on how the target REST API handles requests. To implement idempotency, a custom module would be needed. Such a module would query the Versa Director via its REST API, verify whether the address object already has the desired value, and only perform an update if a change is necessary.

Summary

This document provided an example of how to use Ansible automation in combination with Versa Director. In the example, a configuration change was executed on multiple devices.

There are several other use cases within a Versa environment where Ansible can be effectively applied. A few examples include:

• Generating configuration files: Ansible can generate configuration text files using Jinja2 templates, which can then be copy-pasted into the Versa Director CLI. This approach is particularly useful for one-off, complex changes involving multiple configuration items across several devices, improving both speed and consistency.

• Configuration validation: Ansible can retrieve various configuration elements from Versa Director via REST API and validate their correctness. For instance, it can check whether all devices are configured with the correct NTP server.

• Direct device validation via SSH: Ansible can SSH into VOS devices to perform additional checks. For example, it can validate via the Ubuntu CLI whether the system clock is synchronized with an NTP server.

Powered by BetterDocs