Compare commits
4 Commits
step35/669
...
gemini/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3db489457 | ||
|
|
1b2115a9b8 | ||
|
|
e2564a27ab | ||
|
|
b402335599 |
@@ -165,6 +165,9 @@ security:
|
||||
# Empty list = deny all (secure by default)
|
||||
# Set via env var TIMMY_AUTHOR_WHITELIST as comma-separated list
|
||||
author_whitelist: []
|
||||
fleet_management:
|
||||
fleet_manager_script: scripts/fleet_manager.py
|
||||
|
||||
_config_version: 9
|
||||
session_reset:
|
||||
mode: none
|
||||
|
||||
4
fleet_config.yaml
Normal file
4
fleet_config.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
hosts:
|
||||
- vps1.example.com
|
||||
- vps2.example.com
|
||||
- vps3.example.com
|
||||
87
scripts/fleet_manager.py
Normal file
87
scripts/fleet_manager.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
FLEET_CONFIG_PATH = "fleet_config.yaml"
|
||||
|
||||
def _read_fleet_config():
|
||||
if not os.path.exists(FLEET_CONFIG_PATH):
|
||||
print(f"Error: {FLEET_CONFIG_PATH} not found.")
|
||||
return {"hosts": []}
|
||||
with open(FLEET_CONFIG_PATH, 'r') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def deploy_agent_to_host(host, agent_id, config_path):
|
||||
print(f"Deploying agent {agent_id} with config {config_path} to {host} (simulated)...")
|
||||
# In a real scenario, this would involve SSHing to the host and deploying the agent
|
||||
print(f"Agent {agent_id} deployed to {host} (simulated).")
|
||||
|
||||
def deploy_all_agents(agent_id, config_path):
|
||||
config = _read_fleet_config()
|
||||
hosts = config.get("hosts", [])
|
||||
if not hosts:
|
||||
print("No hosts configured in fleet_config.yaml. Aborting deploy-all.")
|
||||
return
|
||||
|
||||
print(f"Deploying agent {agent_id} to all configured hosts...")
|
||||
for host in hosts:
|
||||
deploy_agent_to_host(host, agent_id, config_path)
|
||||
print("Fleet-wide deployment initiated (simulated).")
|
||||
|
||||
def start_agent(agent_id, config_path):
|
||||
print(f"Starting agent {agent_id} with config {config_path}...")
|
||||
# This is a placeholder for actual agent startup logic
|
||||
# In a real scenario, this would involve SSHing to a VPS and starting a systemd service or docker container
|
||||
print(f"Agent {agent_id} started (simulated).")
|
||||
|
||||
def stop_agent(agent_id):
|
||||
print(f"Stopping agent {agent_id}...")
|
||||
# Placeholder for actual agent stopping logic
|
||||
print(f"Agent {agent_id} stopped (simulated).")
|
||||
|
||||
def list_agents():
|
||||
print("Listing active agents (simulated)...")
|
||||
# Placeholder for listing active agents
|
||||
active_agents = ["agent-1", "agent-2"]
|
||||
for agent in active_agents:
|
||||
print(f"- {agent}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Fleet Manager for Timmy Agents")
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# Start command
|
||||
start_parser = subparsers.add_parser("start", help="Start an agent")
|
||||
start_parser.add_argument("agent_id", help="ID of the agent to start")
|
||||
start_parser.add_argument("--config", default="default", help="Configuration path for the agent")
|
||||
|
||||
# Stop command
|
||||
stop_parser = subparsers.add_parser("stop", help="Stop an agent")
|
||||
stop_parser.add_argument("agent_id", help="ID of the agent to stop")
|
||||
|
||||
# List command
|
||||
list_parser = subparsers.add_parser("list", help="List active agents")
|
||||
|
||||
# Deploy-all command
|
||||
deploy_all_parser = subparsers.add_parser("deploy-all", help="Deploy an agent to all configured hosts")
|
||||
deploy_all_parser.add_argument("agent_id", help="ID of the agent to deploy")
|
||||
deploy_all_parser.add_argument("--config", default="default", help="Configuration path for the agent")
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "start":
|
||||
start_agent(args.agent_id, args.config)
|
||||
elif args.command == "stop":
|
||||
stop_agent(args.agent_id)
|
||||
elif args.command == "list":
|
||||
list_agents()
|
||||
elif args.command == "deploy-all":
|
||||
deploy_all_agents(args.agent_id, args.config)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
130
scripts/fleet_orchestrator.py
Executable file
130
scripts/fleet_orchestrator.py
Executable file
@@ -0,0 +1,130 @@
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
FLEET_HOSTS = os.environ.get("FLEET_HOSTS", "143.198.27.163 104.131.15.18").split()
|
||||
TIMMY_USER = os.environ.get("TIMMY_USER", "root")
|
||||
TIMMY_DIR = os.environ.get("TIMMY_HOME", "/root") + "/timmy"
|
||||
|
||||
|
||||
def run_remote_command(host, command):
|
||||
"""Executes a command remotely on a given host via SSH."""
|
||||
ssh_command = ["ssh", f"{TIMMY_USER}@{host}", command]
|
||||
print(f"Executing on {host}: {' '.join(ssh_command)}")
|
||||
try:
|
||||
result = subprocess.run(ssh_command, capture_output=True, text=True, check=True)
|
||||
print(f"[{host}] STDOUT:\n{result.stdout}")
|
||||
if result.stderr:
|
||||
print(f"[{host}] STDERR:\n{result.stderr}")
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"[{host}] ERROR: Command failed with exit code {e.returncode}")
|
||||
print(f"[{host}] STDOUT:\n{e.stdout}")
|
||||
print(f"[{host}] STDERR:\n{e.stderr}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[{host}] AN UNEXPECTED ERROR OCCURRED: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def deploy_agent(host):
|
||||
"""Deploys the agent on a remote host, including its systemd service and code."""
|
||||
print(f"Deploying agent on {host}...")
|
||||
|
||||
# Ensure the remote directory structure exists
|
||||
run_remote_command(host, f"mkdir -p {TIMMY_DIR}/timmy-home/agent")
|
||||
run_remote_command(host, f"mkdir -p /etc/systemd/system")
|
||||
|
||||
# Copy the timmy-agent.service file
|
||||
subprocess.run(["scp", "configs/timmy-agent.service", f"{TIMMY_USER}@{host}:/etc/systemd/system/timmy-agent.service"], check=True)
|
||||
|
||||
# Copy the agent code
|
||||
# Assuming 'agent' directory is parallel to 'scripts' within 'timmy-home'
|
||||
subprocess.run(["scp", "-r", "agent/", f"{TIMMY_USER}@{host}:{TIMMY_DIR}/timmy-home/"], check=True)
|
||||
|
||||
# Reload systemd, enable and start the service
|
||||
commands = [
|
||||
"systemctl daemon-reload",
|
||||
"systemctl enable timmy-agent.service",
|
||||
"systemctl start timmy-agent.service"
|
||||
]
|
||||
for cmd in commands:
|
||||
run_remote_command(host, cmd)
|
||||
|
||||
|
||||
|
||||
def retire_agent(host):
|
||||
"""Retires the agent on a remote host by stopping, disabling, and removing its systemd service and code."""
|
||||
print(f"Retiring agent on {host}...")
|
||||
commands = [
|
||||
"systemctl stop timmy-agent.service",
|
||||
"systemctl disable timmy-agent.service",
|
||||
"rm -f /etc/systemd/system/timmy-agent.service",
|
||||
"systemctl daemon-reload", # Reload daemon to remove the service from memory
|
||||
f"rm -rf {TIMMY_DIR}/timmy-home/agent" # Optional: remove agent code
|
||||
]
|
||||
for cmd in commands:
|
||||
run_remote_command(host, cmd)
|
||||
|
||||
|
||||
def start_agent(host):
|
||||
"""Starts the timmy-agent.service on a remote host."""
|
||||
print(f"Starting agent on {host}...")
|
||||
run_remote_command(host, f"systemctl start timmy-agent.service")
|
||||
|
||||
|
||||
def stop_agent(host):
|
||||
"""Stops the timmy-agent.service on a remote host."""
|
||||
print(f"Stopping agent on {host}...")
|
||||
run_remote_command(host, f"systemctl stop timmy-agent.service")
|
||||
|
||||
|
||||
def update_agent(host):
|
||||
"""Pulls the latest timmy-home repo and restarts the agent on a remote host."""
|
||||
print(f"Updating agent on {host}...")
|
||||
commands = [
|
||||
f"cd {TIMMY_DIR}/timmy-home && git pull",
|
||||
f"systemctl restart timmy-agent.service"
|
||||
]
|
||||
for cmd in commands:
|
||||
run_remote_command(host, cmd)
|
||||
|
||||
|
||||
def status_agent(host):
|
||||
"""Checks the status of the timmy-agent.service on a remote host."""
|
||||
print(f"Checking agent status on {host}...")
|
||||
run_remote_command(host, f"systemctl status timmy-agent.service --no-pager")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python fleet_orchestrator.py <command> [host]")
|
||||
print("Commands: deploy, start, stop, update, status")
|
||||
sys.exit(1)
|
||||
|
||||
action = sys.argv[1]
|
||||
target_host = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
hosts_to_target = [target_host] if target_host else FLEET_HOSTS
|
||||
|
||||
for host in hosts_to_target:
|
||||
if action == "deploy":
|
||||
deploy_agent(host)
|
||||
elif action == "start":
|
||||
start_agent(host)
|
||||
elif action == "stop":
|
||||
stop_agent(host)
|
||||
elif action == "update":
|
||||
update_agent(host)
|
||||
elif action == "status":
|
||||
status_agent(host)
|
||||
elif action == "retire":
|
||||
retire_agent(host)
|
||||
else:
|
||||
print(f"Unknown command: {action}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
58
tests/test_fleet_manager.py
Normal file
58
tests/test_fleet_manager.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Adjust the path to import fleet_manager from scripts directory
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'scripts')))
|
||||
import fleet_manager
|
||||
|
||||
class TestFleetManager(unittest.TestCase):
|
||||
|
||||
@patch('fleet_manager._read_fleet_config')
|
||||
@patch('builtins.print')
|
||||
def test_deploy_all_agents(self, mock_print, mock_read_config):
|
||||
mock_read_config.return_value = {"hosts": ["host1.example.com", "host2.example.com"]}
|
||||
|
||||
# Mock sys.argv to simulate command line arguments
|
||||
with patch.object(sys, 'argv', ['fleet_manager.py', 'deploy-all', 'test-agent', '--config', 'test-config']):
|
||||
fleet_manager.main()
|
||||
|
||||
mock_print.assert_any_call("Deploying agent test-agent with config test-config to host1.example.com (simulated)...")
|
||||
mock_print.assert_any_call("Deploying agent test-agent with config test-config to host2.example.com (simulated)...")
|
||||
mock_print.assert_any_call("Fleet-wide deployment initiated (simulated).")
|
||||
|
||||
@patch('fleet_manager._read_fleet_config')
|
||||
@patch('builtins.print')
|
||||
def test_deploy_all_agents_no_hosts(self, mock_print, mock_read_config):
|
||||
mock_read_config.return_value = {"hosts": []}
|
||||
|
||||
with patch.object(sys, 'argv', ['fleet_manager.py', 'deploy-all', 'test-agent']):
|
||||
fleet_manager.main()
|
||||
|
||||
mock_print.assert_any_call("No hosts configured in fleet_config.yaml. Aborting deploy-all.")
|
||||
|
||||
@patch('builtins.print')
|
||||
def test_start_agent(self, mock_print):
|
||||
with patch.object(sys, 'argv', ['fleet_manager.py', 'start', 'agent-x', '--config', 'config-x']):
|
||||
fleet_manager.main()
|
||||
mock_print.assert_any_call("Starting agent agent-x with config config-x...")
|
||||
mock_print.assert_any_call("Agent agent-x started (simulated).")
|
||||
|
||||
@patch('builtins.print')
|
||||
def test_stop_agent(self, mock_print):
|
||||
with patch.object(sys, 'argv', ['fleet_manager.py', 'stop', 'agent-y']):
|
||||
fleet_manager.main()
|
||||
mock_print.assert_any_call("Stopping agent agent-y...")
|
||||
mock_print.assert_any_call("Agent agent-y stopped (simulated).")
|
||||
|
||||
@patch('builtins.print')
|
||||
def test_list_agents(self, mock_print):
|
||||
with patch.object(sys, 'argv', ['fleet_manager.py', 'list']):
|
||||
fleet_manager.main()
|
||||
mock_print.assert_any_call("Listing active agents (simulated)...")
|
||||
mock_print.assert_any_call("- agent-1")
|
||||
mock_print.assert_any_call("- agent-2")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user