diff --git a/README.md b/README.md index b546990..64c67b1 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,9 @@ Requirements * Optionally, you may need virtiofsd 1.7.0 (or higher) for better filesystem performance inside the virtme-ng guests. + * Optionally, you may need `socat` for the `--server` and `--client` options, + and the host's kernel should support VSOCK (`CONFIG_VHOST_VSOCK`). + Examples ======== @@ -345,13 +348,13 @@ Examples # vmlinux available in the system. ``` - - Connect to a simple remote shell (`socat` is required): + - Connect to a simple remote shell (`socat` is required, VSOCK will be used): ``` - # Start the vng instance with vsock support: - $ vng --vsock + # Start the vng instance with server support: + $ vng --server # In a separate terminal run the following command to connect to a remote shell: - $ vng --vsock-connect + $ vng --client ``` - Run virtme-ng inside a docker container: diff --git a/virtme/commands/run.py b/virtme/commands/run.py index 6d9da7c..9cb986c 100644 --- a/virtme/commands/run.py +++ b/virtme/commands/run.py @@ -130,32 +130,6 @@ def make_parser() -> argparse.ArgumentParser: help="The MAC address to assign to the NIC interface, e.g. 52:54:00:12:34:56. " + "The last octet will be incremented for the next network devices.", ) - g.add_argument( - "--vsock", - action="store", - nargs="?", - metavar="COMMAND", - const="", - help="Enable a VSock to communicate from the host to the device, 'socat' is required. " - + "An argument can be optionally specified to start a different command.", - ) - g.add_argument( - "--vsock-cid", - action="store", - metavar="CID", - type=int, - default=3, - help="CID for the VSock.", - ) - g.add_argument( - "--vsock-connect", - action="store", - nargs="?", - metavar="COMMAND", - const="", - help="Connect to a VM using VSock. " - + "An argument can be optionally specified to launch this command instead of a prompt.", - ) g.add_argument( "--balloon", action="store_true", @@ -348,6 +322,40 @@ def make_parser() -> argparse.ArgumentParser: g.add_argument( "--nvgpu", action="store", default=None, help="Set guest NVIDIA GPU." ) + + g = parser.add_argument_group(title="Remote Console") + cli_srv_choices = ["console"] + g.add_argument( + "--server", + action="store", + const=cli_srv_choices[0], + nargs="?", + choices=cli_srv_choices, + help="Enable a server to communicate later from the host to the device using '--client'. " + + "By default, a simple console will be offered using a VSOCK connection, and 'socat' for the proxy." + ) + g.add_argument( + "--client", + action="store", + const=cli_srv_choices[0], + nargs="?", + choices=cli_srv_choices, + help="Connect to a VM launched with the '--server' option for a remote control.", + ) + g.add_argument( + "--port", + action="store", + type=int, + default=2222, + help="Unique port to communicate with a VM.", + ) + g.add_argument( + "--remote-cmd", + action="store", + metavar="COMMAND", + help="To start in the VM a different command than the default one (--server), " + + "or to launch this command instead of a prompt (--client).", + ) return parser @@ -865,6 +873,95 @@ def is_subpath(path, potential_parent): return common_path == potential_parent +def get_console_path(port): + return os.path.join(tempfile.gettempdir(), "virtme-console", f"{port}.sh") + + +def console_client(args): + try: + # with tty support + (cols, rows) = os.get_terminal_size() + stty = f'stty rows {rows} cols {cols} iutf8 echo' + socat_in = f'file:{os.ttyname(sys.stdin.fileno())},raw,echo=0' + except OSError: + stty = '' + socat_in = '-' + socat_out = f'VSOCK-CONNECT:{args.port}:1024' + + user = args.user if args.user else '${virtme_user:-root}' + + if args.pwd: + cwd = os.path.relpath(os.getcwd(), args.root) + elif args.cwd is not None: + cwd = os.path.relpath(args.cwd, args.root) + else: + cwd = '${virtme_chdir:+"${virtme_chdir}"}' + + # use 'su' only if needed: another use, or to get a prompt + cmd = f'if [ "{user}" != "root" ]; then\n' + \ + f' exec su "{user}"' + if args.remote_cmd is not None: + exec_escaped = args.remote_cmd.replace('"', '\\"') + cmd += f' -c "{exec_escaped}"' + \ + '\nelse\n' + \ + f' {args.remote_cmd}\n' + else: + cmd += '\nelse\n' + \ + ' exec su\n' + cmd += 'fi' + + console_script_path = get_console_path(args.port) + with open(console_script_path, 'w', encoding="utf-8") as file: + print(( + '#! /bin/bash\n' + 'main() {\n' + f'{stty}\n' + f'HOME=$(getent passwd "{user}" | cut -d: -f6)\n' + f'cd {cwd}\n' + f'{cmd}\n' + '}\n' + 'main' # use a function to avoid issues when the script is modified + ), file=file) + os.chmod(console_script_path, 0o755) + + if args.dry_run: + print('socat', socat_in, socat_out) + else: + os.execvp('socat', ['socat', socat_in, socat_out]) + + +def console_server(args, qemu, arch, qemuargs, kernelargs): + console_script_path = get_console_path(args.port) + if os.path.exists(console_script_path): + arg_fail("console: '%s' file exists: " % console_script_path + + "another VM is running with the same --port? " + + "If not, remove this file.") + + def cleanup_console_script(): + os.unlink(console_script_path) + + # create an empty file that can be populated later on + console_script_dir = os.path.dirname(console_script_path) + os.makedirs(console_script_dir, exist_ok=True) + open(console_script_path, 'w', encoding="utf-8").close() + atexit.register(cleanup_console_script) + + if args.remote_cmd is not None: + console_exec = args.remote_cmd + else: + console_exec = console_script_path + if args.root != "/": + virtfs_config = VirtFSConfig( + path=console_script_dir, + mount_tag="virtme.vsockmount", + ) + export_virtfs(qemu, arch, qemuargs, virtfs_config) + kernelargs.append("virtme_vsockmount=%s" % console_script_dir) + + kernelargs.extend([f"virtme.vsockexec=`{console_exec}`"]) + qemuargs.extend(["-device", "vhost-vsock-pci,guest-cid=%d" % args.port]) + + # Allowed characters in mount paths. We can extend this over time if needed. _SAFE_PATH_PATTERN = "[a-zA-Z0-9_+ /.-]+" _RWDIR_RE = re.compile("^(%s)(?:=(%s))?$" % (_SAFE_PATH_PATTERN, _SAFE_PATH_PATTERN)) @@ -873,56 +970,11 @@ def is_subpath(path, potential_parent): def do_it() -> int: args = _ARGPARSER.parse_args() - vsock_script_path = os.path.join(tempfile.gettempdir(), "virtme-vsock", - f"{args.vsock_cid}.sh") - - if args.vsock_connect is not None: - try: - # with tty support - (cols, rows) = os.get_terminal_size() - stty = f'stty rows {rows} cols {cols} iutf8 echo' - socat_in = f'file:{os.ttyname(sys.stdin.fileno())},raw,echo=0' - except OSError: - stty = '' - socat_in = '-' - socat_out = f'VSOCK-CONNECT:{args.vsock_cid}:1024' + if args.client is not None: + if args.server is not None: + arg_fail('--client cannot be used with --server.') - user = args.user if args.user else '${virtme_user:-root}' - - if args.pwd: - cwd = os.path.relpath(os.getcwd(), args.root) - elif args.cwd is not None: - cwd = os.path.relpath(args.cwd, args.root) - else: - cwd = '${virtme_chdir:+"${virtme_chdir}"}' - - # use 'su' only if needed: another use, or to get a prompt - cmd = f'if [ "{user}" != "root" ]; then\n' + \ - f' exec su "{user}"' - if args.vsock_connect: - exec_escaped = args.vsock_connect.replace('"', '\\"') - cmd += f' -c "{exec_escaped}"' + \ - '\nelse\n' + \ - f' {args.vsock_connect}\n' - else: - cmd += '\nelse\n' + \ - ' exec su\n' - cmd += 'fi' - - with open(vsock_script_path, 'w', encoding="utf-8") as file: - print(( - '#! /bin/bash\n' - 'main() {\n' - f'{stty}\n' - f'HOME=$(getent passwd "{user}" | cut -d: -f6)\n' - f'cd {cwd}\n' - f'{cmd}\n' - '}\n' - 'main' # use a function to avoid issues when the script is modified - ), file=file) - os.chmod(vsock_script_path, 0o755) - - os.execvp('socat', ['socat', socat_in, socat_out]) + console_client(args) sys.exit(0) arch = architectures.get(args.arch) @@ -1461,35 +1513,8 @@ def get_net_mac(index): ] ) - def cleanup_vsock_script(): - os.unlink(vsock_script_path) - - if args.vsock is not None: - if os.path.exists(vsock_script_path): - arg_fail("vsock: '%s' file exists: " % vsock_script_path - + "another VM is running with the same --vsock-cid? " - + "If not, remove this file.") - - # create an empty file that can be populated later on - vsock_script_dir = os.path.dirname(vsock_script_path) - os.makedirs(vsock_script_dir, exist_ok=True) - open(vsock_script_path, 'w', encoding="utf-8").close() - atexit.register(cleanup_vsock_script) - - if args.vsock: - vsock_exec = args.vsock - else: - vsock_exec = vsock_script_path - if args.root != "/": - virtfs_config = VirtFSConfig( - path=vsock_script_dir, - mount_tag="virtme.vsockmount", - ) - export_virtfs(qemu, arch, qemuargs, virtfs_config) - kernelargs.append("virtme_vsockmount=%s" % vsock_script_dir) - - kernelargs.extend([f"virtme.vsockexec=`{vsock_exec}`"]) - qemuargs.extend(["-device", "vhost-vsock-pci,guest-cid=%d" % args.vsock_cid]) + if args.server is not None: + console_server(args, qemu, arch, qemuargs, kernelargs) if args.pwd: rel_pwd = os.path.relpath(os.getcwd(), args.root) diff --git a/virtme_ng/run.py b/virtme_ng/run.py index 10e3e3f..80b57f9 100644 --- a/virtme_ng/run.py +++ b/virtme_ng/run.py @@ -359,34 +359,6 @@ def make_parser(): + "The last octet will be incremented for the next network devices.", ) - parser.add_argument( - "--vsock", - action="store", - nargs="?", - metavar="COMMAND", - const="", - help="Enable a VSock to communicate from the host to the device, 'socat' is required. " - + "An argument can be optionally specified to start a different command.", - ) - - parser.add_argument( - "--vsock-cid", - action="store", - metavar="CID", - type=int, - help="CID for the VSock.", - ) - - parser.add_argument( - "--vsock-connect", - action="store", - nargs="?", - metavar="COMMAND", - const="", - help="Connect to a VM using VSock. " - + "An argument can be optionally specified to launch this command instead of a prompt.", - ) - parser.add_argument( "--disk", "-D", @@ -504,6 +476,44 @@ def make_parser(): metavar="[GPU PCI Address]", help="Add a passthrough NVIDIA GPU", ) + + g_remote = parser.add_argument_group(title="Remote Console") + cli_srv_choices = ["console"] + + g_remote.add_argument( + "--server", + action="store", + const=cli_srv_choices[0], + nargs="?", + choices=cli_srv_choices, + help="Enable a server to communicate later from the host to the device using '--client'. " + + "By default, a simple console will be offered using a VSOCK connection, and 'socat' for the proxy." + ) + + g_remote.add_argument( + "--client", + action="store", + const=cli_srv_choices[0], + nargs="?", + choices=cli_srv_choices, + help="Connect to a VM launched with the '--server' option for a remote control.", + ) + + g_remote.add_argument( + "--port", + action="store", + type=int, + help="Unique port to communicate with a VM.", + ) + + g_remote.add_argument( + "--remote-cmd", + action="store", + metavar="COMMAND", + help="To start in the VM a different command than the default one (--server), " + + "or to launch this command instead of a prompt (--client).", + ) + return parser @@ -973,23 +983,29 @@ def _get_virtme_net_mac_address(self, args): else: self.virtme_param["net_mac_address"] = "" - def _get_virtme_vsock(self, args): - if args.vsock is not None: - self.virtme_param["vsock"] = "--vsock '" + args.vsock + "'" + def _get_virtme_server(self, args): + if args.server is not None: + self.virtme_param["server"] = "--server " + args.server + else: + self.virtme_param["server"] = "" + + def _get_virtme_client(self, args): + if args.client is not None: + self.virtme_param["client"] = "--client " + args.client else: - self.virtme_param["vsock"] = "" + self.virtme_param["client"] = "" - def _get_virtme_vsock_cid(self, args): - if args.vsock_cid is not None: - self.virtme_param["vsock_cid"] = "--vsock-cid " + str(args.vsock_cid) + def _get_virtme_port(self, args): + if args.port is not None: + self.virtme_param["port"] = "--port " + str(args.port) else: - self.virtme_param["vsock_cid"] = "" + self.virtme_param["port"] = "" - def _get_virtme_vsock_connect(self, args): - if args.vsock_connect is not None: - self.virtme_param["vsock_connect"] = "--vsock-connect '" + args.vsock_connect + "'" + def _get_virtme_remote_cmd(self, args): + if args.remote_cmd is not None: + self.virtme_param["remote_cmd"] = "--remote-cmd '" + args.remote_cmd + "'" else: - self.virtme_param["vsock_connect"] = "" + self.virtme_param["remote_cmd"] = "" def _get_virtme_disk(self, args): if args.disk is not None: @@ -1146,9 +1162,10 @@ def run(self, args): self._get_virtme_mods(args) self._get_virtme_network(args) self._get_virtme_net_mac_address(args) - self._get_virtme_vsock(args) - self._get_virtme_vsock_cid(args) - self._get_virtme_vsock_connect(args) + self._get_virtme_server(args) + self._get_virtme_client(args) + self._get_virtme_port(args) + self._get_virtme_remote_cmd(args) self._get_virtme_disk(args) self._get_virtme_sound(args) self._get_virtme_disable_microvm(args) @@ -1188,9 +1205,10 @@ def run(self, args): + f'{self.virtme_param["mods"]} ' + f'{self.virtme_param["network"]} ' + f'{self.virtme_param["net_mac_address"]} ' - + f'{self.virtme_param["vsock"]} ' - + f'{self.virtme_param["vsock_cid"]} ' - + f'{self.virtme_param["vsock_connect"]} ' + + f'{self.virtme_param["server"]} ' + + f'{self.virtme_param["client"]} ' + + f'{self.virtme_param["port"]} ' + + f'{self.virtme_param["remote_cmd"]} ' + f'{self.virtme_param["disk"]} ' + f'{self.virtme_param["sound"]} ' + f'{self.virtme_param["disable_microvm"]} '