While this may not be a typical use case, if you need to run commands within a Guest Virtual Machine using Ansible, this post is for you. Here is a sample playbook. However, this playbook would not work unless a custom module is configured.
- name: Deploy VM using Cloud-init
hosts: localhost
gather_facts: yes
become: yes
tasks:
- name: "Run command on the Guest"
run_vm_command:
domain: test-vm
command: ["apt","update"]
timeout: 30
register: foo
- debug: var=foo
You must create a custom module to talk with the guest VM. If the name of the playbook is “test.yaml” then the module must be created in a directory called “library” at the same level as the playbook.
.
├── library
│ └── run_vm_command.py
└── test.yaml
1 directory, 2 files
The code for run_vm_command.py is as follows:
#!/usr/bin/python3
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
import json
import subprocess
import base64
import sys
import time
from datetime import datetime
'''
To print step-by-step data, use only for debugging
'''
DEBUG=False
'''
just in case you need to print step by step data
'''
def debug_print(data):
if DEBUG:
print(str(data))
'''
Check the command running status
'''
def get_status(module, domain,pid):
#build the JSON body for PID status check
my_cmd_json_pid=json.dumps(([{"execute": "guest-exec-status", "arguments": { "pid": pid }}])[0])
debug_print(my_cmd_json_pid)
#Get the status of the PID
result_pid = subprocess.run(['virsh', '-c','qemu:///system','qemu-agent-command', domain, my_cmd_json_pid], capture_output=True, text=True, encoding="utf-8")
debug_print(result_pid)
if json.loads(result_pid.stdout)["return"]["exited"]:
return True, json.loads(result_pid.stdout)
else:
return False,json.loads(result_pid.stdout)
def run_cmd(module, domain,cmd,args):
if len(args) == 0:
args=[]
my_cmd_json=json.dumps([{"execute": "guest-exec", "arguments": { "path": str(cmd), "arg": args, "capture-output": True }}][0])
debug_print(my_cmd_json)
# Run the command, with above JSON
cmd_run_result = subprocess.run(['virsh', '-c','qemu:///system','qemu-agent-command', domain, my_cmd_json], capture_output=True, text=True, encoding="utf-8")
if cmd_run_result.returncode != 0:
print(cmd_run_result.stderr,file=sys.stderr)
sys.exit(cmd_run_result.returncode)
debug_print(cmd_run_result)
#extract the PID from then returned json body
try:
pid=json.loads(cmd_run_result.stdout)["return"]["pid"]
debug_print(pid)
except Exception as e:
print(e)
print([cmd_run_result, e],file=sys.stderr)
sys.exit(cmd_run_result.returncode)
return cmd_run_result, pid
def parse_stdout_stderr(module, raw_out):
try:
base64_out=base64.b64decode(raw_out["return"]["out-data"]).decode('UTF-8')
except KeyError as e:
base64_out=""
try:
base64_err=base64.b64decode(raw_out["return"]["err-data"]).decode('UTF-8')
except KeyError as e:
base64_err=""
return base64_out, base64_err
def main():
module_args = dict(
domain=dict(type='str', required=True),
command=dict(type='list', required=True),
timeout=dict(type='int', required=True),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
result = dict(
msg = '',
stdout = '',
stdout_lines = [],
stderr = '',
stderr_lines = [],
rc = 0,
failed = False,
changed=False
)
domain = module.params['domain']
command = module.params['command']
command_timeout = module.params['timeout']
cmd=command[0]
args=command[1:]
#running the command
result_cmd, pid = run_cmd(module,domain,cmd,args)
#wait till command is finished,check every 1 sec
cmd_exited=False
wait_counter=0
start_time = datetime.now()
while not cmd_exited:
cmd_exited, raw_out = get_status(module, domain, pid)
time_delta = datetime.now() - start_time
if time_delta.total_seconds() >= float(command_timeout):
result['stderr'] = f"Error: Command timed out"
result['rc']=99
module.exit_json(**result)
time.sleep(1)
stdout, stderr = parse_stdout_stderr(module, raw_out)
rc = raw_out["return"]["exitcode"]
if rc != 0:
result['failed']=True
result['msg'] = raw_out
result['stdout'] = stdout.rstrip()
result['stdout_lines'] = stdout.rstrip().split('\n')
result['stderr'] = stderr.rstrip()
result['stderr_lines'] = stderr.rstrip().split('\n')
result['rc']=rc
module.exit_json(**result)
sys.exit(rc)
if __name__ == '__main__':
main()
Now, if the playbook is executed, the output is as follows:
ansible-playbook test.yaml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [Deploy VM using Cloud-init] *********************************************************************************************************************************
TASK [Gathering Facts] ********************************************************************************************************************************************
[Sun Jan 15 00:26:37 2023]
ok: [localhost]
TASK [Run command on the Guest] ***********************************************************************************************************************************
[Sun Jan 15 00:26:38 2023]
ok: [localhost]
TASK [debug] ******************************************************************************************************************************************************
[Sun Jan 15 00:26:42 2023]
ok: [localhost] => {
"foo": {
"changed": false,
"failed": false,
"msg": {
"return": {
"err-data": "CldBUk5JTkc6IGFwdCBkb2VzIG5vdCBoYXZlIGEgc3RhYmxlIENMSSBpbnRlcmZhY2UuIFVzZSB3aXRoIGNhdXRpb24gaW4gc2NyaXB0cy4KCg==",
"exitcode": 0,
"exited": true,
"out-data": "SGl0OjEgaHR0cDovL2FyY2hpdmUudWJ1bnR1LmNvbS91YnVudHUgamFtbXkgSW5SZWxlYXNlCkdldDoyIGh0dHA6Ly9hcmNoaXZlLnVidW50dS5jb20vdWJ1bnR1IGphbW15LXVwZGF0ZXMgSW5SZWxlYXNlIFsxMTQga0JdCkdldDozIGh0dHA6Ly9zZWN1cml0eS51YnVudHUuY29tL3VidW50dSBqYW1teS1zZWN1cml0eSBJblJlbGVhc2UgWzExMCBrQl0KR2V0OjQgaHR0cDovL2FyY2hpdmUudWJ1bnR1LmNvbS91YnVudHUgamFtbXktYmFja3BvcnRzIEluUmVsZWFzZSBbOTkuOCBrQl0KRmV0Y2hlZCAzMjQga0IgaW4gMXMgKDQ0NiBrQi9zKQpSZWFkaW5nIHBhY2thZ2UgbGlzdHMuLi4KQnVpbGRpbmcgZGVwZW5kZW5jeSB0cmVlLi4uClJlYWRpbmcgc3RhdGUgaW5mb3JtYXRpb24uLi4KNCBwYWNrYWdlcyBjYW4gYmUgdXBncmFkZWQuIFJ1biAnYXB0IGxpc3QgLS11cGdyYWRhYmxlJyB0byBzZWUgdGhlbS4K"
}
},
"rc": 0,
"stderr": "\nWARNING: apt does not have a stable CLI interface. Use with caution in scripts.",
"stderr_lines": [
"",
"WARNING: apt does not have a stable CLI interface. Use with caution in scripts."
],
"stdout": "Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease\nGet:2 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [114 kB]\nGet:3 http://security.ubuntu.com/ubuntu jammy-security InRelease [110 kB]\nGet:4 http://archive.ubuntu.com/ubuntu jammy-backports InRelease [99.8 kB]\nFetched 324 kB in 1s (446 kB/s)\nReading package lists...\nBuilding dependency tree...\nReading state information...\n4 packages can be upgraded. Run 'apt list --upgradable' to see them.",
"stdout_lines": [
"Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease",
"Get:2 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [114 kB]",
"Get:3 http://security.ubuntu.com/ubuntu jammy-security InRelease [110 kB]",
"Get:4 http://archive.ubuntu.com/ubuntu jammy-backports InRelease [99.8 kB]",
"Fetched 324 kB in 1s (446 kB/s)",
"Reading package lists...",
"Building dependency tree...",
"Reading state information...",
"4 packages can be upgraded. Run 'apt list --upgradable' to see them."
]
}
}
PLAY RECAP ********************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0