0

I've got a couple of servers running each few virtual machines. On each server I use Vagrant to create and control VMs. Now I want to find some tool (thought about Ansible cause I know it a little bit) to control all VMs on all servers.

For example, I've got Git repo with Vagrantfile and all servers have clones of it. Now I manually execute git pull and vagrant provision on each server if I change something in Vagrantfile and I want to automate not only this case, but all Vagrant-related actions which need to be executed on all VMs on all servers.

Googled about it, but all links are about using Ansible as Vagrant provisioner, not vice versa. I understand that I could just run shell commands using Ansible on all servers, but I don't think it is really good solution, it's a kind of "bashsible", but I need more universal and multifunctional solution.

2 Answers2

1

I understand that I could just run shell commands using Ansible on all servers, but I don't think it is really good solution

Which would imply some kind of Vagrant-aware Ansible module. I am not aware of any shipped with Ansible; you could write one. Or, settle for shell commands and run Vagrant with Ansible command module.


ansible-pull is an example of how to implement pull style similar to what you do now. From cron, each host pulls and runs a playbook, that pulls the Vagrant repo and runs that.

Or, do it push style with ansible-playbook from a management host.

John Mahowald
  • 32,050
  • 2
  • 19
  • 34
  • Thank you! Followed official Ansible modules development guide https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html, looks like it is exactly what I want. I'created little "hello world" demo module and now want to start implementing real functionality, but could not find how to get inventory (hosts list) in module code anywhere. Do you know how to do it? – Dmitriy Vinokurov Dec 27 '19 at 10:16
  • Oops, my mistake. Module is executed for each host, specified in inventory and I don't need to know on which host is module executed. – Dmitriy Vinokurov Dec 27 '19 at 10:53
  • Hm, I think that hostname is still needed. Module behaviour depends on it. Could not find how to get it using internal Ansible tools. Of course I could execute `cat /etc/hostname` and get it's output, but it looks like dirty hack. Is there another way? – Dmitriy Vinokurov Dec 27 '19 at 11:21
  • Found it by myself in https://stackoverflow.com/questions/35643000/ansible-api-custom-module and https://stackoverflow.com/questions/29026094/ansible-access-host-group-vars-from-within-custom-module. I should pass hostname explicitly to module. Example of ad hoc command - `ansible 192.168.3.61 -m vagrant -a 'command=halt remote_host={{ inventory_hostname }}' -u user --ask-pass -i hosts`. – Dmitriy Vinokurov Dec 27 '19 at 11:39
  • Consider answering your own question. Although, I'm not clear on what you are attempting to do, as vagrant is not a module shipped with ansible 2.9. If writing your own module, explaining how you did it would be appreciated. – John Mahowald Dec 27 '19 at 16:48
  • Yes, I've created own `vagrant` module. Thanks for suggestion, I'll prepare and post own answer based on this experience soon. – Dmitriy Vinokurov Dec 28 '19 at 13:45
  • Done - https://serverfault.com/a/998831/191594 – Dmitriy Vinokurov Jan 14 '20 at 07:19
0

After a bit of research I've solved my issue. Here is how I did it.

Before code a little note about architecture for what it was created - I've got a couple of servers running headless VirtualBoxes (ci_vm_servers section in hosts file, for example 192.168.3.1), each of them run Vagrant to create and control few virtual machines (ci_vm_nodes section in hosts file, for example 192.168.3.11).

Now, the code (based on https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html). Following example code shows module which can execute commands, for example pull Vagrant repo, on all VM servers at once.

#!/usr/bin/python

# Copyright: (c) 2019, Dmitriy Vinokurov <gim6626@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

ANSIBLE_METADATA = {
    'metadata_version': '0.1',
    'status': ['preview'],
    'supported_by': 'community'
}

DOCUMENTATION = '''
---
module: vagrant

short_description: Ansible Vagrant module

version_added: "2.9"

description:
    - "Module to control remote servers running Vagrant"

options:
    command:
        description:
            - This is the command to send to the remote servers
        required: true

author:
    - Dmitriy Vinokurov (@gim6626)
'''

RETURN = r''' # '''

from ansible.module_utils.basic import AnsibleModule

def run_module():
    # define available arguments/parameters a user can pass to the module
    module_args = dict(
        command=dict(type='str', required=True),
        remote_host=dict(type='str', required=True),
    )

    # seed the result dict in the object
    # we primarily care about changed and state
    # change 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,
        original_message='',
        message=''
    )

    # 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=True
    )

    # if the user is working with this module in only check mode we do not
    # want to make any changes to the environment, just return the current
    # state with no modifications
    if module.check_mode:
        module.exit_json(**result)

    command = module.params['command']
    remote_host = module.params['remote_host']

    vagrant_manager = VagrantManager(module, remote_host, command, result)

    if command == 'pull':
        vagrant_manager.pull_vagrant_repo_on_vm_server()
    elif command == 'provision':
        vagrant_manager.vagrant_provision_on_vm_server()
    else:
        module.fail_json(msg='Unsupported command "{}"'.format(command),
                         **vagrant_manager.result)

    # use whatever logic you need to determine whether or not this module
    # made any modifications to your target
    result['changed'] = True

    # in the event of a successful module execution, you will want to
    # simple AnsibleModule.exit_json(), passing the key/value results
    module.exit_json(**result)

class VagrantManager:

    SUBCOMMAND_ERROR_MSG_TEMPLATE = 'Subcommand "{}" failed while trying to execute "{}" command'
    INVALID_REMOTE_HOST_TYPE_ERROR_MSG_TEMPLATE = 'Invalid remote host "{}" type, expected {}'
    VM_SERVER_TYPE = 'VM Server'
    VM_NODE_TYPE = 'VM Node'

    def __init__(self, module, remote_host, command, result):
        self.module = module
        self.remote_host = remote_host
        self.command = command
        self.result = result

        # manipulate or modify the state as needed (this is going to be the
        # part where your module will do what it needs to do)
        self.result['original_message'] = 'Execute "{}"'.format(self.command)
        self.result['command'] = self.command
        self.result['message'] = 'OK'
        self.result['remote_host'] = self.remote_host

    def pull_vagrant_repo_on_vm_server(self):
        self._check_if_remote_host_is_vm_server()
        self._run_sub_command('git pull', '/home/vbox/ci-vagrant')

    def vagrant_provision_on_vm_server(self):
        self._check_if_remote_host_is_vm_server()
        self._run_sub_command('vagrant provision', '/home/vbox/ci-vagrant')

    def _run_sub_command(self, sub_command, cwd=None):
        rc, stdout, stderr = self.module.run_command(sub_command, cwd=cwd)
        if rc != 0:
            self.result['stdout'] = stdout
            self.result['stderr'] = stderr
            self.module.fail_json(msg=self.SUBCOMMAND_ERROR_MSG_TEMPLATE.format(sub_command, self.command),
                                  **self.result)


    def _check_if_remote_host_is_vm_server(self):
        remote_host_type = self._guess_remote_host_type()
        if remote_host_type != VagrantManager.VM_SERVER_TYPE:
            module.fail_json(msg=INVALID_REMOTE_HOST_TYPE_ERROR_MSG_TEMPLATE(module.params['remote_host'], VM_SERVER_TYPE),
                            **result)

    def _check_if_remote_host_is_vm_node(self):
        remote_host_type = self._guess_remote_host_type()
        if remote_host_type != VagrantManager.VM_NODE_TYPE:
            module.fail_json(msg=INVALID_REMOTE_HOST_TYPE_ERROR_MSG_TEMPLATE(module.params['remote_host'], VM_NODE_TYPE),
                            **result)

    def _guess_remote_host_type(self):
        if '192.168.3.' not in self.remote_host:
            self.module.fail_json(msg='Wrong remote host "{}", it looks like neither VM server nor VM node'.format(self.remote_host),
                                  **self.result)
        elif len(self.remote_host.split('.')[-1]) == 1:
            return VagrantManager.VM_SERVER_TYPE
        else:
            return VagrantManager.VM_NODE_TYPE

def main():
    run_module()

if __name__ == '__main__':
    main()

To prepare execution:

  1. Follow instructions on https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html#common-environment-setup
  2. Create file lib/ansible/modules/cloud/vagrant/vagrant.py in cloned repo with content shown above

Finally, you could pull Vagrant repo on all servers using ansible ci_vm_servers -m vagrant -a 'command=pull remote_host={{ inventory_hostname }}' -u vbox --ask-pass -i hosts, assuming that: 1) you have ci_vm_servers section with IPs list in hosts file as noted above; 2) user vbox on all servers is set to pull without specifying password.

There are only commands for servers in example, not for VMs, but it could be easily improved. up/halt/reboot commands to execute for all VMs on all servers also could be added without problems.