|
| 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. |
0 commit comments