Simplifying VM Management: Executing Commands in Guest VMs using Virsh

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)

  1. Root permissions to run virsh on the host machine.
  2. 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:
    1. https://manpages.ubuntu.com/manpages/bionic/man8/qemu-ga.8.html
    2. https://pve.proxmox.com/wiki/Qemu-guest-agent
    5 1 vote
    Please, Rate this post
    Subscribe
    Notify of
    guest
    0 Comments
    Inline Feedbacks
    View all comments
    0
    Would love your thoughts, please comment.x
    ()
    x
    Scroll to Top