Skip to content

Commit 4f35491

Browse files
authored
Merge pull request #4158 from YongxueHong/multi-host-test-vt-agent
VT Agent: Add agent for remote task execution
2 parents a233d95 + 5a779a6 commit 4f35491

18 files changed

Lines changed: 1960 additions & 0 deletions

File tree

avocado_vt/vt_agent/pyproject.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[build-system]
2+
requires = ["setuptools>=64.0.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "avocado-vt-agent"
7+
version = "0.1.0"
8+
requires-python = ">=3.8"
9+
authors = [
10+
{name = "Yongxue Hong", email = "[email protected]"},
11+
{name = "Xu Han", email = "[email protected]"},
12+
{name = "Zhenchao Liu", email = "[email protected]"},
13+
]
14+
maintainers = [
15+
{name = "Yongxue Hong", email = "[email protected]"},
16+
{name = "Xu Han", email = "[email protected]"},
17+
{name = "Zhenchao Liu", email = "[email protected]"},
18+
]
19+
description = "Avocado Agent for Virtualization Testing"
20+
readme = "README.md"
21+
22+
[project.urls]
23+
Homepage = "https://avocado-vt.readthedocs.io/en/latest/"
24+
Documentation = "https://avocado-vt.readthedocs.io/en/latest/index.html#"
25+
Repository = "https://github.com/avocado-framework/avocado-vt"
26+
27+
[tool.setuptools.packages.find]
28+
where = ["src"]
29+
include = ["avocado_vt.agent"]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Avocado VT Agent
2+
3+
The Avocado VT Agent is a lightweight, extensible RPC agent designed to be installed and run on remote test machines. It allows for the remote execution of predefined functions and custom services, facilitating test automation within the Avocado VT framework.
4+
5+
This agent is packaged as a separate, standalone distribution (`avocado-vt-agent`) that installs itself into the `avocado_vt` namespace.
6+
7+
## Installation
8+
9+
Before running the agent, it must be installed using `pip`. This is typically done on a remote machine where tests will be executed.
10+
11+
### Installing the Agent
12+
13+
To build and install the agent, navigate to its directory (the one containing `pyproject.toml`) and use `pip`:
14+
15+
```bash
16+
# From the .../avocado-vt/avocado_vt/vt_agent/ directory
17+
pip install .
18+
```
19+
20+
This command will build the agent and install it into your Python environment.
21+
22+
## Running the Agent
23+
24+
Once installed, the agent can be started as a module. It's recommended to run it as a background service for continuous operation.
25+
26+
```bash
27+
python -m avocado_vt.agent --host <address> --port <port> --pid-file /path/to/agent.pid
28+
```
29+
30+
**Command-line Arguments:**
31+
32+
* `--host <address>`: The IP address for the agent to listen on. Defaults to `127.0.0.1` (localhost only for security).
33+
* `--port <port>`: The port number for the agent to listen on. Defaults to `9999`.
34+
* `--pid-file <path>`: (Required) Path to write the agent's Process ID (PID).
35+
36+
**Example:**
37+
```bash
38+
python -m avocado_vt.agent --host 0.0.0.0 --port 8001 --pid-file ./vt_agent.pid &
39+
```
40+
41+
## Features
42+
43+
* XML-RPC based communication.
44+
* Dynamically loads custom services from the `services` directory.
45+
* Threaded server to handle multiple client requests concurrently.
46+
* Logging for agent operations and service execution.
47+
48+
## Checking Agent Status
49+
50+
You can check if the agent is alive and responsive using an XML-RPC client to call the `core.is_alive` method.
51+
52+
**Example Python client:**
53+
```python
54+
import xmlrpc.client
55+
56+
try:
57+
# Replace 'localhost' and '8001' with the agent's host and port
58+
agent_proxy = xmlrpc.client.ServerProxy("http://localhost:8001/", allow_none=True)
59+
60+
if agent_proxy.core.is_alive():
61+
print("Agent is alive and responding.")
62+
else:
63+
print("core.is_alive() returned False (unexpected).")
64+
65+
except ConnectionRefusedError:
66+
print("Connection refused. Is agent running at the specified address and port?")
67+
except xmlrpc.client.Fault as fault:
68+
print(f"RPC Fault: {fault.faultCode} - {fault.faultString}")
69+
except Exception as e:
70+
print(f"An error occurred: {e}")
71+
72+
# Example of calling an example service method
73+
try:
74+
# Assumes the 'examples.hello' service is loaded
75+
print(f"Executing ping to the agent host")
76+
greeting = agent_proxy.examples.hello.ping()
77+
print(f"Service Response: {greeting}")
78+
except xmlrpc.client.Fault as fault:
79+
print(f"RPC Fault calling service: {fault.faultCode} - {fault.faultString}")
80+
except Exception as e:
81+
print(f"Error calling service: {e}")
82+
```
83+
84+
## Architecture Overview
85+
86+
* **Core Agent (`core/`)**: Handles RPC server setup, request dispatching, service loading, logging, and data directory management.
87+
* **Application (`app/`)**: Manages command-line arguments and the main execution flow.
88+
* **Services (`services/`)**: Contains dynamically loaded service modules. Each `.py` file (not `__init__.py`) in this directory (and its subdirectories) is treated as a service module. Functions within these modules become callable RPC methods, namespaced by their module path (e.g., `examples.hello.say_hello`).
89+
90+
## Developing Services
91+
92+
1. **Create a Python file** (e.g., `my_service.py`) inside the `src/avocado_vt/agent/services/` directory or a subdirectory.
93+
2. **Define functions** within your Python file. These functions will be exposed as RPC methods.
94+
* Example `my_service.py`:
95+
```python
96+
import logging
97+
98+
from avocado_vt.agent.core.logger import DEFAULT_LOG_NAME
99+
100+
LOG = logging.getLogger(f"{DEFAULT_LOG_NAME}." + __name__)
101+
102+
def do_something(param1, param2="default"):
103+
LOG.info(f"my_service.do_something called with: {param1}, {param2}")
104+
result = f"Processed {param1} and {param2}"
105+
return {"status": "success", "result": result}
106+
```
107+
3. **Logging**: Use `logging.getLogger(f"{DEFAULT_LOG_NAME}." + __name__)` to get a logger instance that integrates with the agent's logging.
108+
4. **Naming**: If your file is `services/custom/my_service.py`, its functions will be callable like `custom.my_service.do_something`.
109+
5. The agent will automatically discover and load your service when it starts.
110+
111+
## Remote Logging
112+
113+
The agent provides a core API to stream logs from services back to a client:
114+
* `core.start_log_redirection(host, port)`: Tells the agent to start sending logs to a socket listener at the specified `host` and `port`.
115+
* `core.stop_log_redirection()`: Stops the log streaming.
116+
117+
A corresponding log server/listener needs to be running on the client side to receive these logs.
118+
119+
## Security Considerations
120+
121+
* **Default Binding**: The agent now defaults to binding on `127.0.0.1` (localhost only) for improved security. To allow external connections, explicitly specify `--host 0.0.0.0`.
122+
* **Network Access**: When binding to `0.0.0.0`, ensure proper firewall rules and network security controls are in place.
123+
* **Service Loading**: The agent dynamically loads Python modules from the services directory. Only place trusted service modules in this directory.

avocado_vt/vt_agent/src/avocado_vt/agent/__init__.py

Whitespace-only changes.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# This program is free software; you can redistribute it and/or modify
2+
# it under the terms of the GNU General Public License as published by
3+
# the Free Software Foundation; either version 2 of the License, or
4+
# (at your option) any later version.
5+
#
6+
# This program is distributed in the hope that it will be useful,
7+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9+
#
10+
# See LICENSE for more details.
11+
#
12+
# Copyright: Red Hat Inc. 2025
13+
# Authors: Yongxue Hong <[email protected]>
14+
15+
import logging
16+
import os
17+
import shutil
18+
19+
# pylint: disable=E0611
20+
from avocado_vt.agent.app.args import init_arguments
21+
from avocado_vt.agent.app.cmd import run
22+
from avocado_vt.agent.core import data_dir as core_data_dir
23+
from avocado_vt.agent.core.logger import DEFAULT_LOG_NAME, init_logger
24+
25+
26+
def cleanup_previous_run(dirs):
27+
"""
28+
Clean up directories from previous agent runs.
29+
30+
:param dirs: Tuple of directory paths to clean up
31+
:type dirs: tuple
32+
"""
33+
for directory in dirs:
34+
if os.path.exists(directory):
35+
try:
36+
shutil.rmtree(directory)
37+
except OSError:
38+
pass
39+
40+
41+
def setup_directories(dirs, logger):
42+
"""
43+
Create necessary directories for agent operation.
44+
45+
:param dirs: Tuple of directory paths to create
46+
:type dirs: tuple
47+
:param logger: Logger instance for reporting
48+
:type logger: logging.Logger
49+
"""
50+
for directory in dirs:
51+
try:
52+
os.makedirs(directory, exist_ok=True)
53+
except OSError as e:
54+
logger.error("Failed to create directory %s: %s", directory, e)
55+
raise
56+
57+
58+
def main():
59+
"""Main entry point for the agent."""
60+
args = init_arguments()
61+
62+
data_dir = core_data_dir.get_data_dir()
63+
log_dir = core_data_dir.get_log_dir()
64+
download_dir = core_data_dir.get_download_dir()
65+
dirs = (data_dir, log_dir, download_dir)
66+
67+
cleanup_previous_run(dirs)
68+
69+
init_logger()
70+
logger = logging.getLogger(f"{DEFAULT_LOG_NAME}.__main__")
71+
72+
setup_directories(dirs, logger)
73+
74+
run(args.host, args.port, args.pid_file)
75+
76+
77+
if __name__ == "__main__":
78+
main()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
The `app` package for the Avocado VT Agent.
3+
4+
This package contains modules related to the application setup,
5+
command-line argument parsing, and main execution command for the agent.
6+
"""
7+
8+
# pylint: disable=E0611
9+
from avocado_vt.agent.app import args, cmd

0 commit comments

Comments
 (0)