This post will help anyone who is attempting to use ansible to modify files on VMs running on ESXI via automation when Practicing DevOps. This method assumes that you have cloned out a machine, but do not yet have network access to that machine meaning you would need to utilize native ESXi methods to get communication setup.

I use methods like this in my home lab to help stay current with new trends in DevOps Not everyone is going to be fortunate enough to have cloud playgrounds to test out new things, and home labs are a great way to make a one-time investment in your future.

The code for the project can be found here!

Related Articles

Prerequisites

This article is going to assume that you already have VMWare running in a home lab somewhere. This also assumes that you have some familiarity with VMware and networking.

This post will also assume that you have already installed a baseline OS running open-vm-tools. Packer is a good tool to use to automate your baseline operating system installation to create a reusable disk image.

You may also want to read this post to get a good feeling about how to automate cloning images when vSphere or Center are not available solutions.

Terms

  • ansible_host: A subkey of an invnetory definition that sets an alternative way to target a server in inventory.
  • connection: local: At the playbook level, you can set connection: local to ensure that all connections are being sent to localhost (much like ansible delegate_to). I almost never use this simply because I generally want control over my connections at the task level, not the playbook level.
  • delegate_to: A majority of this playbook is setup with delegate_to: localhost. This means that the delegated host running the Ansible script will also execute anything with the defined delegate_to: localhost.
  • Delegated host: If ServerA is the machine with a terminal you are running Ansible from, and your target is ServerB; if you also set delegate_to: ServerB, Then ServerB is also the delegated host which is running the ansible code. Ansible is passing the action off from the server where ansible was initiated from to a different remote host.
  • Inventory hostname: Ansible-playbooks requires that inventory be setup for group targeting. Each host is defined with a hostname as a top level key. This term is a bit of misnomer since you can give Ansible any hostname you want and configure ansible_host as an alternative targeting mechanic, like IP address.
  • Remote Host / Remote Machine: The server you are targeting remotely with Ansible

The Playbook

Below is the full code to copy SSH keys to a newly cloned machine, and set an IP address with a NetworkManager restart.

The general idea is to run ansible-playbook modify-vmware-esxi-guest-file.yaml -i inventory.yaml from your command line. The playbook will then connect to ESXi and create a .ssh folder on your fresh machine. It will then copy your local /home/user/.ssh/id_rsa.pub file to the remote machine. Finally, it will run /bin/nmcli commands to get a static IP address set on the VM.

Since the remote machine from inventory exists but is not yet able to be connected to delegate_to: localhost is necessary of you would immediately receive an ansible connection error.


---
- name: Modify VMWare ESXi VM File
  hosts: all
  gather_facts: false
  tasks:
    - include_vars: config.yaml
      delegate_to: localhost

    - include_vars: creds.yaml # Remember, this should be in a vault
      delegate_to: localhost   # These creds are for example use only

    - name: Create .ssh folder
      community.vmware.vmware_guest_file_operation:
        validate_certs: false
        hostname: "{{ vmware_host }}"
        username: "{{ vmware_user }}"
        password: "{{ vmware_password }}"
        datacenter: ha-datacenter
        vm_id: "{{ vmware.name }}"
        vm_username: "{{ centos_template_user }}"
        vm_password: "{{ centos_template_password }}"
        directory:
          path: "/home/user/.ssh/"
          operation: create
          recurse: no
      delegate_to: localhost

    - name: Copy RSA to VM
      community.vmware.vmware_guest_file_operation:
        validate_certs: false
        hostname: "{{ vmware_host }}"
        username: "{{ vmware_user }}"
        password: "{{ vmware_password }}"
        datacenter: ha-datacenter
        vm_id: "{{ vmware.name }}"
        vm_username: "{{ centos_template_user }}"
        vm_password: "{{ centos_template_password }}"
        copy:
          src: "/home/user/.ssh/id_rsa.pub"
          dest: "/home/user/.ssh/authorized_keys"
          overwrite: yes
      delegate_to: localhost

    - name: Run command inside a virtual machine
      community.vmware.vmware_vm_shell:
        hostname: "{{ vmware_host }}"
        username: "{{ vmware_user }}"
        password: "{{ vmware_password }}"
        datacenter: ha-datacenter
        vm_id: "{{ vmware.name }}"
        vm_username: "{{ centos_template_user }}"
        vm_password: "{{ centos_template_password }}"
        vm_shell: "{{ item.shell }}"
        vm_shell_args: "{{ item.args }}"
      loop:
        - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.ignore-auto-dns yes" }
        - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.address \"{{ vmware.ip_address }}/24\"" }
        - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.gateway \"{{ vmware.gateway_address }}\"" }
        - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.dns \"{{ vmware.dns_address }}\"" }
        - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.method manual" }
        - { shell: "/bin/systemctl", args: "NetworkManager restart" }
      delegate_to: localhost

Task By Task Breakdown

To not rehash details I have gone through in other articles, you can find a breakdown of the inventory, credentials, and config in this post.

The first step of this playbook is to create a users .ssh folder so an authorized_keys file can be created. The playbook makes an assumption that your user is named… user, but that could be modified and turned into a configuration parameter pretty quickly. I will break down each of the config parameters below:

  • validate_certs is set to false because most of us are not going to be using trusted 3rd party certs in our VMware environment. If you are doing this in a professional environment, ensure you have valid certs installed and leave validate_certs set to the default of true. validate_certs should also be set to false if you are connecting to your VMWare server via IP address since your cert will not match an IP address.
  • vmware_user and vmware_password should be set to a user that has SSH access to your VMWare server. I would not recommend that you do this in any sort of production environment, but it is perfectly fine for a home lab.
  • datacenter is set to ha-datacenter. This is the default name of a "datacenter" in ESXi. There is no reason to change this.
  • vm_id is either the exact case-sensitive name of the virtual machine being configured. If you created your machine using ansible, then make sure you map the name you gave it here via variables.
  • vm_username and vm_password is in reference to the user you setup when creating your template or disk image. ESXi needs this to effectively run an interactive login to the server to perform your commands.
  • directory is signifying that our action is going to be taken on a directory. In the next task you will see that we use the copy key instead of directory but we are utilizing the exact same module.
    • directory.path is the path on the ESXi guests that you want to take action on.
    • directory.operation denotes what we want to have happen. In this case, we are going to create a directory.
    • directory.recurse tells ansible if it should send a command that recursively creates directories. If your default user is not named user, you would end up with a /home/user/.ssh directory that will be fairly useless.

- name: Create .ssh folder
  community.vmware.vmware_guest_file_operation:
    validate_certs: false
    hostname: "{{ vmware_host }}"
    username: "{{ vmware_user }}"
    password: "{{ vmware_password }}"
    datacenter: ha-datacenter
    vm_id: "{{ vmware.name }}"
    vm_username: "{{ centos_template_user }}"
    vm_password: "{{ centos_template_password }}"
    directory:
      path: "/home/user/.ssh/"
      operation: create
      recurse: no
  delegate_to: localhost

This next task does a copy of a file from the machine you executed the ansible playbook from onto the ESXi guest that we are configuring. In the interest of brevity, I am not going to rehash the duplicate keys from above. Here are the specific keys of interest in this task:

  • copy denotes that we want to copy a file. The file needs to exist on your source system and will be copied to the ESXi guest.
  • copy.src is the path to the source file. As you can see, we are targeting the public key here.
  • copy.dest is the full path to the destination. As you can see we are targeting an authorized_keys file as the destination. This could be dangerous if that file already exists as it will be overwritten.
  • copy.overwrite ensures that we will clobber whatever exists on the destination system with the file from our source system.

- name: Copy RSA to VM
  community.vmware.vmware_guest_file_operation:
    validate_certs: false
    hostname: "{{ vmware_host }}"
    username: "{{ vmware_user }}"
    password: "{{ vmware_password }}"
    datacenter: ha-datacenter
    vm_id: "{{ vmware.name }}"
    vm_username: "{{ centos_template_user }}"
    vm_password: "{{ centos_template_password }}"
    copy:
      src: "/home/user/.ssh/id_rsa.pub"
      dest: "/home/user/.ssh/authorized_keys"
      overwrite: yes
  delegate_to: localhost

This last task is composed a bit differently. Effectively it is being used to run a series of nmcli commands along with a single systemctl command to set a static IP address for this machine. I have this task composed this way to clean up ansible code. This could be written as six fully independent tasks in ansible, but remember to stay DRY and don't repeat yourself. Here is a breakdown of the specific keys of interest:

  • vm_shell represents the exact path to the command that is going to be run on the ESXi guest. Do not include command arguments in this field, the documentation specifically states that it wants a full path to an executable.
  • vm_shell_args represents the arguments to the vm_shell command.
  • loop controls how the task will loop through the data defined underneath. I have the keys setup as dictionary objects so that we get both shell executables and arguments from the loop.

- name: Run command inside a virtual machine
  community.vmware.vmware_vm_shell:
    hostname: "{{ vmware_host }}"
    username: "{{ vmware_user }}"
    password: "{{ vmware_password }}"
    datacenter: ha-datacenter
    vm_id: "{{ vmware.name }}"
    vm_username: "{{ centos_template_user }}"
    vm_password: "{{ centos_template_password }}"
    vm_shell: "{{ item.shell }}"
    vm_shell_args: "{{ item.args }}"
  loop:
    - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.ignore-auto-dns yes" }
    - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.address \"{{ vmware.ip_address }}/24\"" }
    - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.gateway \"{{ vmware.gateway_address }}\"" }
    - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.dns \"{{ vmware.dns_address }}\"" }
    - { shell: "/bin/nmcli", args: "connection modify ens192 IPv4.method manual" }
    - { shell: "/bin/systemctl", args: "NetworkManager restart" }
  delegate_to: localhost

Conclusion

As you can see, with a fairly concise set of steps, you can level up your DevOps automation game by rapidly configuring virtual machines in your home lab.