If you are using virsh/qemu for Virtual machine management, then there are many occasions when an administrator requires to run some commands on the guest VM. This is made very simple by “Quem-guest-agent.” This post demos two approaches to running a command in the Virtual guest machine.
- Approach 1: Run the command directly on the host machine using shell commands, which is ideal for those who want to understand the underlying process.
- Approach 2: Utilize a script to abstract the steps, perfect for those who just want to run the command without delving into the technical details.”
Prerequisite:(Teststed for Ubuntu Host and Guest Machine)
- Root permissions to run virsh on the host machine.
- qemu-guest-agent installed on the guest VM. You can run the following three commands on the guest VM for installation:
apt-get install qemu-guest-agent
systemctl enable qemu-guest-agent
systemctl start qemu-guest-agent
Approach-1: Running the commands Manually
For example, an administrator needs to install any software on the guest VM called “test-vm
“. For this administrator need to run the “apt install cowsay -y
” command on the guest VM.
Step-1: Run the command using guest-exec
The guest-exec subcommand of qemu-guest-agent takes the argument of the binary that needs to be executed and its arguments. So, in this case, the binary is “apt
,” and its arguments are “install cowsay -y
“. Since we want to capture the output, keep the bool “Capture-output” to true.
virsh -c qemu:///system qemu-agent-command test-vm '{"execute": "guest-exec", "arguments": { "path": "apt", "arg": [ "install","cowsay","-y" ], "capture-output": true }}' --pretty
The above command would return a JSON with a PID of the process, and you can use tools like jq
to parse out the PID.
{
"return": {
"pid": 1993
}
}
Step 2: Check the Status of the executed command
To check the status of the command executed, we will run the guest-exec-status
subcommand and supply the PID retrieved from the previous command.
virsh -c qemu:///system qemu-agent-command test-vm '{"execute": "guest-exec-status", "arguments": { "pid": 1993 }}' --pretty
The above command would generate a JSON file like the one below; This JSON provides some crucial information about the executed command. You can use tools like jq
to parse out the data if you are going to do scripting. 1. exitcode:
This contains the return code to know if the command is successful(If 0) or failed(if not 0).2. err-data:
This contains the data from the stderr stream of the command in base64 encoded format. 3. out-data:
This contains the data from the stdout stream of the command output in base64 encoded format. 4. exited:
This is a boolean; this signifies whether the command is completed(either failed or successful).
{
"return": {
"exitcode": 0,
"err-data": "CldBUk5JTkc6IGFwdCBkb2VzIG5vdCBoYXZlIGEgc3RhYmxlIENMSSBpbnRlcmZhY2UuIFVzZSB3aXRoIGNhdXRpb24gaW4gc2NyaXB0cy4KCg==",
"out-data": "UmVhZGluZyBwYWNrYWdlIGxpc3RzLi4uCkJ1aWxkaW5nIGRlcGVuZGVuY3kgdHJlZS4uLgpSZWFkaW5nIHN0YXRlIGluZm9ybWF0aW9uLi4uCmNvd3NheSBpcyBhbHJlYWR5IHRoZSBuZXdlc3QgdmVyc2lvbiAoMy4wMytkZnNnMi04KS4KMCB1cGdyYWRlZCwgMCBuZXdseSBpbnN0YWxsZWQsIDAgdG8gcmVtb3ZlIGFuZCA0IG5vdCB1cGdyYWRlZC4K",
"exited": true
}
}
Step-3: Decode the base64 encoded stdout and stderr
echo -n "CldBUk5JTkc6IGFwdCBkb2VzIG5vdCBoYXZlIGEgc3RhYmxlIENMSSBpbnRlcmZhY2UuIFVzZSB3aXRoIGNhdXRpb24gaW4gc2NyaXB0cy4KCg==" |base64 -d
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
echo -n "UmVhZGluZyBwYWNrYWdlIGxpc3RzLi4uCkJ1aWxkaW5nIGRlcGVuZGVuY3kgdHJlZS4uLgpSZWFkaW5nIHN0YXRlIGluZm9ybWF0aW9uLi4uCmNvd3NheSBpcyBhbHJlYWR5IHRoZSBuZXdlc3QgdmVyc2lvbiAoMy4wMytkZnNnMi04KS4KMCB1cGdyYWRlZCwgMCBuZXdseSBpbnN0YWxsZWQsIDAgdG8gcmVtb3ZlIGFuZCA0IG5vdCB1cGdyYWRlZC4K" |base64 -d
Reading package lists...
Building dependency tree...
Reading state information...
cowsay is already the newest version (3.03+dfsg2-8).
0 upgraded, 0 newly installed, 0 to remove and 4 not upgraded.
Approach-2: Automating the Steps in Approach-1
I am not a python expert, but something like this could be used to abstract all the steps and let the user only bother about the command and the name of the target VM over which the command needs to be executed. You can find the script at the end of this section.
#Example of listing files at /var/log directory on a VM called 'test-vm'
python3 run_cmd_on_guest.py test-vm "ls -lrt /var/log/"
total 912
drwxr-xr-x 2 root root 4096 Oct 14 09:29 dist-upgrade
-rw-rw---- 1 root utmp 0 Dec 18 20:10 btmp
drwxr-sr-x+ 3 root systemd-journal 4096 Jan 13 18:14 journal
drwx------ 2 root root 4096 Jan 13 18:14 private
-rw-r----- 1 root adm 64729 Jan 13 18:14 dmesg.0
-rw-r--r-- 1 root root 7198 Jan 13 18:15 alternatives.log
drwxr-xr-x 2 landscape landscape 4096 Jan 13 19:51 landscape
-rw-r--r-- 1 syslog adm 193123 Jan 14 20:23 cloud-init.log
-rw-r----- 1 root adm 26936 Jan 14 20:23 cloud-init-output.log
-rw-r----- 1 root adm 65165 Jan 14 20:23 dmesg
-rw-r--r-- 1 root root 766 Jan 14 20:26 ubuntu-advantage-timer.log
-rw-r----- 1 syslog adm 157515 Jan 14 20:29 kern.log
-rw-rw-r-- 1 root utmp 292292 Jan 14 20:42 lastlog
-rw-rw-r-- 1 root utmp 11136 Jan 14 20:46 wtmp
drwxr-x--- 2 root adm 4096 Jan 14 21:11 unattended-upgrades
drwxr-xr-x 2 root root 4096 Jan 14 21:31 apt
-rw-r--r-- 1 root root 29651 Jan 14 21:31 dpkg.log
-rw-r----- 1 syslog adm 13822 Jan 14 22:17 auth.log
-rw-r----- 1 syslog adm 305147 Jan 14 23:00 syslog
#Example of printing the current username on a VM called 'test-vm'
python3 run_cmd_on_guest.py test-vm "whoami"
root
#Example of showing hostname on a VM called 'test-vm'
python3 run_cmd_on_guest.py test-vm "hostname"
test-vm
#Example showing installation of 'cowsay' on a VM called 'test-vm'
python3 run_cmd_on_guest.py test-vm "apt install cowsay -y"
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
Reading package lists...
Building dependency tree...
Reading state information...
cowsay is already the newest version (3.03+dfsg2-8).
0 upgraded, 0 newly installed, 0 to remove and 4 not upgraded.
The script: Feel free to improve it.
#!/usr/bin/python3
#file name run_cmd_on_guest.py
#Author: Technekey.com
"""Module is used to exit with rc"""
import sys
"""Module is used to run virsh commands"""
import subprocess
"""This module is used for sleep during looping"""
import time
"""This module is used for looping with timeout"""
from datetime import datetime
"""Module is used for loading JSON and parsing it"""
import json
"""Module is used to decode base64 encoded data"""
import base64
#To print step-by-step data, use only for debugging
DEBUG=False
try:
DOMAIN=str(sys.argv[1])
except IndexError as e:
print("""The 1st arg should be the Guest VM name
Example: ./script.py vm-name 'ls -lrt /tmp' <timeout>sec""")
sys.exit(1)
try:
command=str(sys.argv[2])
except IndexError as e:
print("""The 2nd arg should be the command to execute on Guest VM.
Example: ./script.py vm-name 'ls -lrt /tmp' <timeout>sec""")
sys.exit(2)
#3rd arg can be used to set the command timeout value, fallback to 600 sec if not set.
try:
command_timeout=sys.argv[3]
except IndexError as e:
command_timeout=600
cmd=command.split()[0]
args=command.split()[1:]
'''
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(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)
debug_print(f"virsh -c qemu:///system qemu-agent-command {DOMAIN} {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(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(f"virsh -c qemu:///system qemu-agent-command {DOMAIN} {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(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
#running the command
result,pid = run_cmd(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(DOMAIN, pid)
time_delta = datetime.now() - start_time
if time_delta.total_seconds() >= float(command_timeout):
print("Error: Command timed out",file=sys.stderr)
sys.exit(99)
break
time.sleep(1)
stdout, stderr = parse_stdout_stderr(raw_out)
rc = raw_out["return"]["exitcode"]
print(stderr.rstrip(),file=sys.stderr)
print(stdout.rstrip())
rc=1
sys.exit(rc)
Summary and References:
- https://manpages.ubuntu.com/manpages/bionic/man8/qemu-ga.8.html
- https://pve.proxmox.com/wiki/Qemu-guest-agent