如何使用 Ansible 控制运行 Vagrant 的 VM 服务器?

如何使用 Ansible 控制运行 Vagrant 的 VM 服务器?

我有几台服务器,每台都运行着几个虚拟机。在每台服务器上,我都使用 Vagrant 创建和控制虚拟机。现在我想找一些工具(想到了 Ansible,因为我对它有点了解)来控制所有服务器上的所有虚拟机。

例如,我有一个 Git 仓库,Vagrantfile并且所有服务器都有它的克隆。现在,如果我更改了某些内容,我会在每台服务器上手动执行git pull,并且我不仅想自动化这种情况,还想自动化所有需要在所有服务器上的所有虚拟机上执行的与 Vagrant 相关的操作。vagrant provisionVagrantfile

谷歌搜索了一下,但所有链接都是关于使用 Ansible 作为 Vagrant 配置器,而不是相反。我知道我可以在所有服务器上使用 Ansible 运行 shell 命令,但我不认为这是一个真正好的解决方案,它是一种“bashsible”,但我需要更通用、多功能的解决方案。

答案1

我知道我可以在所有服务器上使用 Ansible 运行 shell 命令,但我不认为这是一个好的解决方案

这意味着需要某种 Vagrant 感知型 Ansible 模块。我不知道 Ansible 是否附带任何模块;您可以编写一个。或者,使用 shell 命令并使用 Ansible 命令模块运行 Vagrant。


ansible-pull是一个如何实现类似于您现在所做的拉取样式的示例。从 cron 中,每个主机都会拉取并运行一个剧本,该剧本会拉取 Vagrant 存储库并运行它。

或者,使用 ansible-playbook管理主机的推送方式。

答案2

经过一番研究,我解决了这个问题。以下是我解决的方法。

在编写代码之前,先简单介绍一下它所创建的架构 - 我有几个运行无头 VirtualBox 的服务器(例如,文件ci_vm_servers中的部分),每个服务器都运行 Vagrant 来创建和控制一些虚拟机(例如,文件中的部分)。hosts192.168.3.1ci_vm_nodeshosts192.168.3.11

现在,代码(基于https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html)。以下示例代码显示了可以同时在所有 VM 服务器上执行命令(例如 pull Vagrant repo)的模块。

#!/usr/bin/python

# Copyright: (c) 2019, Dmitriy Vinokurov <[email protected]>
# 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()

准备执行:

  1. 按照说明进行操作https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html#common-environment-setup
  2. 在克隆的 repo 中创建文件lib/ansible/modules/cloud/vagrant/vagrant.py,内容如上所示

最后,你可以在所有服务器上使用拉取 Vagrant repo ansible ci_vm_servers -m vagrant -a 'command=pull remote_host={{ inventory_hostname }}' -u vbox --ask-pass -i hosts,假设:1)如上所示,文件ci_vm_servers中有一个包含 IP 列表的部分;2)所有服务器上的用户都设置为拉取而不指定密码。hostsvbox

示例中只有针对服务器的命令,没有针对虚拟机的命令,但可以轻松改进。up/halt/reboot还可以毫无问题地添加针对所有服务器上的所有虚拟机执行的命令。

相关内容