One of the highlights of the latest Veeam announcement is the Veeam Backup & Replication 11 RestAPI. Until now a RESTful API was only available for other products from Veeam, e.g. Veeam Availablity Orchestrator. Only through Veeam Enterprise Manager could users make some RESTful API calls for Veeam Backup & Replication. The focus of the Veeam Enterprise Manager RESTful API is more user-centric – Backup, Restore, add Tenant, and so on. The new Veeam Backup & Replication 11 RestAPI is currently mainly focused on infrastructure operations – add Managed Servers, create Backup Repositories, and so on. To leverage the new RestAPI fully integrated in the existing processes I have decided to create a Veeam Backup & Replication RestAPI Ansible Collection.
A few months ago I already tried to create a Veeam Ansible module based on the PowerShell Snap-In. Basically this worked, but unfortunately, the complexity was very high with this approach.
The implementation looks pretty modern and shippes with a Swagger-UI to explore the Veeam Backup & Replication 11 RestAPI (https://<VBR-Server>:9419/static/index.html).

Working with the Veeam RestAPI
There are two simple Get Requests in the Service-Scope of the RestAPI available that do not require authentication, which are good Requests to check the general availability of the API endpoint itself. I typically use the “getServerTime” Request to verify the connection.
curl --location --request GET 'https://10.0.2.16:9419/api/v1/serverTime' \ --header 'x-api-version: 1.0-rev1' \ --header 'Accept: application/json'

For all other Requests (except the authentication itself) an authorization token is required. The Veeam Backup & Replication 11 RestAPI uses Bearer Token.
Veeam RestAPI Authentication
To create a new access token a simple post request with credentials in the data is required.
curl --location --request POST 'https://10.0.2.16:9419/api/oauth2/token' \ --header 'x-api-version: 1.0-rev1' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=password' \ --data-urlencode 'username=Administrator' \ --data-urlencode 'password=<Password>'

With the “access_token” from the response body all further requests can be authorized.
curl --location --request GET 'https://10.0.2.16:9419/api/v1/credentials' \ --header 'x-api-version: 1.0-rev1' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <access_token>'
If you use a tool like Postman you can automatically parse the response of the authentication request to handle further authorization. Postman uses “Tests” (scripts to process the response – See documentation for more details) to add values to the current environment (collection of variables and values).
var jsonData = JSON.parse(responseBody); postman.setEnvironmentVariable("bearerToken", jsonData.access_token);


The first Veeam RestAPI Ansible Module
With this basic knowledge about the Veeam Backup & Replication 11 RestAPI, we are now able to create our first Python-based Ansible Module.
If you are new to Ansible development you should have a look at this Guide: Ansible module development: getting started
To make the Veeam Backup & Replication RestAPI Ansible Collection more portable I have decided to create an Ansible Collection. Collections are the new distribution format for Ansible content and can be published through Ansible Galaxy.
My first module needs some basic error handling for the RestAPI request and should return the current Server certificate.
#!/usr/bin/python # Copyright: (c) 2020, Your Name <[email protected]> # 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 DOCUMENTATION = r''' --- module: veeam_vbr_servercertificate_info short_description: Get Current Veeam Backup Server Certificate from RestAPI. version_added: "1.0.0" description: Get Current Veeam Backup Server Certificate from RestAPI. options: validate_certs: description: Validate SSL certs required: false default: false type: bool server_name: description: VBR Server Name or IP required: true type: str server_port: description: VBR RestAPI Sever Port required: false default: 9419 type: str author: - Markus Kraus (@vMarkusK) ''' EXAMPLES = r''' # Pass in a message - name: Test Veeam RestAPI Collection hosts: localhost tasks: - name: Test veeam_vbr_servercertificate_info veeamhub.veeam_rest.veeam_vbr_servercertificate_info: server_name: '10.0.2.16' register: testout - name: Debug Result debug: var: testout ''' import json import re from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url def run_module(): # define available arguments/parameters a user can pass to the module module_args = dict( server_name=dict(type='str', required=True), server_port=dict(type='str', default='9419'), validate_certs=dict(type='bool', default='False') ) # seed the result dict in the object # we primarily care about changed and state # changed is if this module effectively modified the target # state will include any data that you want your module to pass back # for consumption, for example, in a subsequent task result = dict( changed=False ) # the AnsibleModule object will be our abstraction working with Ansible # this includes instantiation, a couple of common attr would be the # args/params passed to the execution, as well as if the module # supports check mode module = AnsibleModule( argument_spec=module_args, supports_check_mode=False ) request_server = module.params['server_name'] request_port = module.params['server_port'] headers = { 'accept': 'application/json', 'x-api-version': '1.0-rev1', 'Authorization': 'true' } request_url = 'https://' + request_server + ':' + request_port + '/api/v1/serverCertificate' method = "Get" req, info = fetch_url(module, request_url, headers=headers, method=method) if info['status'] != 200: module.fail_json(msg="Fail: %s" % ("Status: " + str(info['status']) + ", Message: " + str(info['msg']))) try: result['servercertificate'] = json.loads(req.read()) except AttributeError: module.fail_json(msg='Parsing Response Failed', **result) module.exit_json(**result) def main(): run_module() if __name__ == '__main__': main()
The playbook for this module looks pretty simple, only the IP / FQDN of the Veeam Backup & Replication Server is required, the “validate_certs” parameter is optional and the default is set to “false”.
- name: Test Veeam RestAPI Collection hosts: localhost tasks: - name: Test veeam_vbr_servercertificate_info veeamhub.veeam_rest.veeam_vbr_servercertificate_info: server_name: '10.0.2.16' register: testout - name: Debug Result debug: var: testout

Veeam RestAPI Ansible Module with Authentication
The required Python code gets a bit more complex when it comes to authentication. We need way more inputs and the response of one RestAPI Request needs to be handed over to another one.
The following Veeam Backup & Replication RestAPI Ansible Collection should return all configured Backup Repositories.
#!/usr/bin/python # Copyright: (c) 2020, Your Name <[email protected]> # 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 DOCUMENTATION = r''' --- module: veeam_vbr_repositories_info short_description: Get Veeam Backup & Replication Repositories. version_added: "1.0.0" description: Get Veeam Backup & Replication Repositories. options: validate_certs: description: Validate SSL certs. required: false default: false type: bool server_name: description: VBR Server Name or IP required: true type: str server_port: description: VBR RestAPI Sever Port required: false default: 9419 type: str server_username: description: VBR Server Username required: true type: str server_password: description: VBR Server password required: true type: str author: - Markus Kraus (@vMarkusK) ''' EXAMPLES = r''' # Pass in a message - name: Test Veeam RestAPI Collection hosts: localhost tasks: - name: Test veeam_vbr_repositories_info veeamhub.veeam_rest.veeam_vbr_repositories_info: server_name: '10.0.2.16' server_username: 'Administrator' server_password: '<Password>' register: testout - name: Debug Result debug: var: testout ''' import json import re from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url def run_module(): # define available arguments/parameters a user can pass to the module module_args = dict( server_name=dict(type='str', required=True), server_username=dict(type='str', required=True), server_password=dict(type='str', required=True, no_log=True), server_port=dict(type='str', default='9419'), validate_certs=dict(type='bool', default='false') ) # seed the result dict in the object # we primarily care about changed and state # changed is if this module effectively modified the target # state will include any data that you want your module to pass back # for consumption, for example, in a subsequent task result = dict( changed=False ) # the AnsibleModule object will be our abstraction working with Ansible # this includes instantiation, a couple of common attr would be the # args/params passed to the execution, as well as if the module # supports check mode module = AnsibleModule( argument_spec=module_args, supports_check_mode=False ) # Authenticate request_server = module.params['server_name'] request_port = module.params['server_port'] request_username = module.params['server_username'] request_password = module.params['server_password'] payload = 'grant_type=password&username=' + request_username + '&password=' + request_password headers = { 'accept': 'application/json', 'x-api-version': '1.0-rev1', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'true' } request_url = 'https://' + request_server + ':' + request_port + '/api/oauth2/token' method = "Post" req, info = fetch_url(module, request_url, headers=headers, method=method, data=payload) if info['status'] != 200: module.fail_json(msg="Fail: %s" % ("Status: " + str(info['status']) + ", Message: " + str(info['msg']))) try: resp = json.loads(req.read()) except AttributeError: module.fail_json(msg='Parsing Response Failed', **result) # Payload headers = { 'x-api-version': '1.0-rev1', 'Authorization': 'Bearer ' + resp['access_token'] } request_url = 'https://' + request_server + ':' + request_port + '/api/v1/backupInfrastructure/repositories' method = "Get" req, info = fetch_url(module, request_url, headers=headers, method=method) if info['status'] != 200: module.fail_json(msg="Fail: %s" % ("Status: " + str(info['status']) + ", Message: " + str(info['msg']))) try: result['infrastructure_repositories'] = json.loads(req.read()) except AttributeError: module.fail_json(msg='Parsing Response Failed', **result) module.exit_json(**result) def main(): run_module() if __name__ == '__main__': main()
Because of the additional inputs for the authentication, the Playbook for the veeam_vbr_repositories_info module looks also a bit more complex.
- name: Test Veeam RestAPI Collection hosts: localhost tasks: - name: Test veeam_vbr_repositories_info veeamhub.veeam_rest.veeam_vbr_repositories_info: server_name: '10.0.2.16' server_username: 'Administrator' server_password: '<Password>' register: testout - name: Debug Result debug: var: testout

Veeam RestAPI Ansible Module with Changes
The next complexity level is adding or changing something. The first object you typically need to add when you create a new Veeam Backup & Replication infrastructure are Credentials (VMware vCenter, Managed Server, and so on). So, the first Veeam Backup & Replication RestAPI Ansible Collection with changes is for credential management.
The veeam_vbr_credentials module should be able to delete and add credentials. Changing existing Credentials is on the roadmap.
#!/usr/bin/python # Copyright: (c) 2020, Your Name <[email protected]> # 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 DOCUMENTATION = r''' --- module: veeam_vbr_credentials short_description: Add and Remove Veeam Backup & Replication Credentials. version_added: "1.0.0" description: Add and Remove Veeam Backup & Replication Credentials. options: validate_certs: description: Validate SSL certs. required: false default: false type: bool state: description: - Indicate desired state. default: present type: str choices: ['present', 'absent'] server_name: description: VBR Server Name or IP required: true type: str server_port: description: VBR RestAPI Sever Port required: false default: 9419 type: str server_username: description: VBR Server Username required: true type: str server_password: description: VBR Server password required: true type: str id: description: VBR Server credential ID required: false type: str username: description: VBR Server credential username required: false type: str password: description: VBR Server credential password required: false type: str description: description: VBR Server credential description required: false type: str type: description: - Set to C(windows) to create new windows credentials. - Set to C(linux) to create new liniux credentials. - Set to C(standard) to create new standard credentials. type: str choices: [ windows, linux, standard ] default: standard author: - Markus Kraus (@vMarkusK) ''' EXAMPLES = r''' # Pass in a message - name: Test Veeam RestAPI Collection hosts: localhost tasks: - name: Test veeam_vbr_credentials Create veeamhub.veeam_rest.veeam_vbr_credentials: server_name: '10.0.2.16' server_username: 'Administrator' server_password: 'Anfang!!' type: 'linux' username: 'root' password: '<Password>' description: 'Created by Ansible RestAPI Module' register: create_cred - name: Debug Result debug: var: create_cred - name: Test veeam_vbr_credentials Delete veeamhub.veeam_rest.veeam_vbr_credentials: server_name: '10.0.2.16' server_username: 'Administrator' server_password: 'Anfang!!' id: "{{ create_cred.msg.id }}" state: absent register: delete_cred - name: Debug Result debug: var: delete_cred ''' import json import re from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url def run_module(): # define available arguments/parameters a user can pass to the module module_args = dict( server_name=dict(type='str', required=True), server_username=dict(type='str', required=True), server_password=dict(type='str', required=True, no_log=True), server_port=dict(type='str', default='9419'), state=dict(type="str", choices=("absent", "present"), default="present"), id=dict(type='str', required=False), username=dict(type='str', required=False), password=dict(type='str', required=False, no_log=True), type=dict(type='str', choices=("windows", "linux", "standard"), default='standard'), description=dict(type='str', required=False), validate_certs=dict(type='bool', default='false') ) required_if_args = [ ["state", "present", ["username", "password"]], ["state", "absent", ["id"]] ] required_together_args = [ ["password", "username"] ] # seed the result dict in the object # we primarily care about changed and state # changed is if this module effectively modified the target # state will include any data that you want your module to pass back # for consumption, for example, in a subsequent task result = dict( changed=False ) # the AnsibleModule object will be our abstraction working with Ansible # this includes instantiation, a couple of common attr would be the # args/params passed to the execution, as well as if the module # supports check mode module = AnsibleModule( argument_spec=module_args, required_if=required_if_args, required_together=required_together_args, supports_check_mode=False ) # General state = module.params['state'] request_server = module.params['server_name'] request_port = module.params['server_port'] # Authenticate request_username = module.params['server_username'] request_password = module.params['server_password'] payload = 'grant_type=password&username=' + request_username + '&password=' + request_password headers = { 'accept': 'application/json', 'x-api-version': '1.0-rev1', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'true' } request_url = 'https://' + request_server + ':' + request_port + '/api/oauth2/token' method = "Post" req, info = fetch_url(module, request_url, headers=headers, method=method, data=payload) if info['status'] != 200: module.fail_json(msg="Fail: %s" % ("Status: " + str(info['status']) + ", Message: " + str(info['msg']))) try: resp = json.loads(req.read()) except AttributeError: module.fail_json(msg='Parsing Response Failed', **result) # Payload if state == 'present': username = module.params['username'] password = module.params['password'] credtype = module.params['type'] description = module.params['description'] body = { 'type': credtype, 'username': username, 'password': password, 'description': description } bodyjson = json.dumps(body) headers = { 'x-api-version': '1.0-rev1', 'Authorization': 'Bearer ' + resp['access_token'], 'Content-Type': 'application/json' } request_url = 'https://' + request_server + ':' + request_port + '/api/v1/credentials' method = "Post" req, info = fetch_url(module, request_url, headers=headers, method=method, data=bodyjson) if info['status'] != 200: module.fail_json(msg="Fail: %s" % ("Status: " + str(info['status']) + ", Message: " + str(info['msg']))) try: result['msg'] = json.loads(req.read()) result['changed'] = True except AttributeError: module.fail_json(msg='Parsing Response Failed', **result) if state == 'absent': credid = module.params['id'] headers = { 'x-api-version': '1.0-rev1', 'Authorization': 'Bearer ' + resp['access_token'] } request_url = 'https://' + request_server + ':' + request_port + '/api/v1/credentials/' + credid method = "get" req, info = fetch_url(module, request_url, headers=headers, method=method) if info['status'] == 200: method = "Delete" req, info = fetch_url(module, request_url, headers=headers, method=method) if info['status'] != 200: module.fail_json(msg="Fail: %s" % ("Status: " + str(info['status']) + ", Message: " + str(info['msg']))) try: result['changed'] = True except AttributeError: module.fail_json(msg='Parsing Response Failed', **result) module.exit_json(**result) def main(): run_module() if __name__ == '__main__': main()
The playbook for the veeam_vbr_credentials module requires different parameters, depending on the requested State (“absent” or “present”).
- Present: Creates new Credentials and requires “username”, “password” and “type”
- Absent: Deletes existing Credentials and requires “id”
- name: Test Veeam RestAPI Collection hosts: localhost tasks: - name: Test veeam_vbr_credentials Create veeamhub.veeam_rest.veeam_vbr_credentials: server_name: '10.0.2.16' server_username: 'Administrator' server_password: '<Password>' type: 'Linux' username: 'root' password: '<Password>' description: 'Created by Ansible RestAPI Module' register: create_cred - name: Debug Result debug: var: create_cred - name: Test veeam_vbr_credentials Delete veeamhub.veeam_rest.veeam_vbr_credentials: server_name: '10.0.2.16' server_username: 'Administrator' server_password: '<Password>' id: "{{ create_cred.msg.id }}" state: absent register: delete_cred - name: Debug Result debug: var: delete_cred

Ansible Module Testing
One of the simplest code validation for Ansible Modules are Sanity Tests. Sanity tests are made up of scripts and tools used to perform static code analysis. The primary purpose of these tests is to enforce Ansible coding standards and requirements.

GitHub Action for Ansible Module Testing
With GitHub Actions, you can create a simple CI to verify your code on each commit or pull request (in my case the Sanity tests). My use case for GitHub Actions is just a small part of the capabilities this CI/CD pipeline offers.

# README FIRST # 1. replace "veeamhub/veeam_rest" with the correct name, ie "community/zabbix" # 2. If you don't have unit tests remove that section # 3. If your collection depends on other collections ensure they are installed, see "Install collection dependencies" # If you need help please ask in #ansible-devel on Freenode IRC name: BasicLint on: # Run CI against all pushes (direct commits, also merged PRs), Pull Requests push: pull_request: # Uncomment the following two lines to run CI once per day (at 06:00 UTC) # schedule: # - cron: '0 6 * * *' jobs: ### # Sanity tests (REQUIRED) # # https://docs.ansible.com/ansible/latest/dev_guide/testing_sanity.html sanity: name: Sanity (Ⓐ${{ matrix.ansible }}) strategy: matrix: ansible: # It's important that Sanity is tested against all stable-X.Y branches # Testing against `devel` may fail as new tests are added. # - stable-2.9 # Only if your collection supports Ansible 2.9 - stable-2.10 - devel runs-on: ubuntu-latest steps: # ansible-test requires the collection to be in a directory in the form # .../ansible_collections/veeamhub/veeam_rest/ - name: Check out code uses: actions/checkout@v2 with: path: ansible_collections/veeamhub/veeam_rest - name: Set up Python uses: actions/setup-python@v2 with: # it is just required to run that once as "ansible-test sanity" in the docker image # will run on all python versions it supports. python-version: 3.8 # Install the head of the given branch (devel, stable-2.10) - name: Install ansible-base (${{ matrix.ansible }}) run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check # run ansible-test sanity inside of Docker. # The docker container has all the pinned dependencies that are required # and all python versions ansible supports. - name: Run sanity tests run: ansible-test sanity --docker -v --color working-directory: ./ansible_collections/veeamhub/veeam_rest
With the use of a basic CI pipeline, you are able to guarantee a baseline of the code quality for each Pull request.

Veeam RestAPI Postman Collection
I have published the current version of my Postman Collection that handles the authentication via script as a GitHub Gist: REST API service for Veeam backup & Replication Postman Collection

Feel free reach out for a newer version as soon as Veeam Backup & Replication 11 is GA.
Veeam RestAPI Ansible Collection
The Veeam Backup & Replication RestAPI Ansible Module is publicly available as a GitHub Repository. All contributions and feedback are welcome.
As soon as the Modules and the Collection reaches a valuable and properly validated level I will publish on Ansible Galaxy.
The full documentation, including the parameters, of the individual module is included in the python file. The inline documentation can be displayed by using: ansible-doc veeam_vbr_credentials_info -M ./plugins/modules/
veeam_vbr_servercertificate_info
Get Current Veeam Backup Server Certificate from RestAPI.
Example:
- name: Test Veeam RestAPI Collection hosts: localhost gather_facts: false tasks: - name: Test veeam_vbr_servercertificate_info veeamhub.veeam_rest.veeam_vbr_servercertificate_info: server_name: '10.0.2.16' register: testout - name: Debug Result debug: var: testout
veeam_vbr_credentials_info
Get Veeam Backup & Replication Credentials.
Example:
- name: Test Veeam RestAPI Collection hosts: localhost tasks: - name: Test veeam_vbr_credentials_info veeamhub.veeam_rest.veeam_vbr_credentials_info: server_name: '10.0.2.16' server_username: 'Administrator' server_password: '<Password>' register: testout - name: Debug Result debug: var: testout
veeam_vbr_credentials
Add and Remove Veeam Backup & Replication Credentials.
Example:
- name: Test Veeam RestAPI Collection hosts: localhost gather_facts: false tasks: - name: Test veeam_vbr_credentials Create veeamhub.veeam_rest.veeam_vbr_credentials: server_name: '10.0.2.16' server_username: 'Administrator' server_password: '<Password>' type: 'Linux' username: 'root' password: '<Password>' description: 'Created by Ansible RestAPI Module' register: create_cred - name: Debug Result debug: var: create_cred - name: Test veeam_vbr_credentials Delete veeamhub.veeam_rest.veeam_vbr_credentials: server_name: '10.0.2.16' server_username: 'Administrator' server_password: '<Password>' id: "{{ create_cred.msg.id }}" state: absent register: delete_cred - name: Debug Result debug: var: delete_cred
veeam_vbr_repositories_info
Get Veeam Backup & Replication Repositories.
Example:
- name: Test Veeam RestAPI Collection hosts: localhost tasks: - name: Test veeam_vbr_repositories_info veeamhub.veeam_rest.veeam_vbr_repositories_info: server_name: '10.0.2.16' server_username: 'Administrator' server_password: '<Password>' register: testout - name: Debug Result debug: var: testout
veeam_vbr_managedservers_info
Get Veeam Backup & Replication Managed Servers.
Example:
- name: Test Veeam RestAPI Collection hosts: localhost tasks: - name: Test veeam_vbr_managedservers_info veeamhub.veeam_rest.veeam_vbr_managedservers_info: server_name: '10.0.2.16' server_username: 'Administrator' server_password: '<Password>' register: testout - name: Debug Result debug: var: testout
Great job!
I started writing my first custom module and collections like you! Get to know the api, work with it locally (I used curl instead of postman), then build it in Python.
A suggestion: The authentication code is (probably) always the same. You should refactor your modules and move the code into a common class (see “module_utils” in ansible) and include it in the modules.
We did this with our icinga-director collection.
Here’s the class:
https://github.com/T-Systems-MMS/ansible-collection-icinga-director/blob/master/plugins/module_utils/icinga.py
Here’s the import:
https://github.com/T-Systems-MMS/ansible-collection-icinga-director/blob/master/plugins/modules/icinga_command.py#L219-L221
Here’s the call to the class:
https://github.com/T-Systems-MMS/ansible-collection-icinga-director/blob/master/plugins/modules/icinga_command.py#L284
Hi Sebastian,
Thanks for your feedback and the great examples.
I was also thinking about moving some stuff to classes. This also makes it possible to use the ansible turbo module: https://github.com/vMarkusK/veeam_rest-ansible/issues/27
Thanks, now I learned about the turbo module! 🙂