
Huawei Cloud Ascend Remote Connect
- 68 installs
- 19 repo stars
- Updated July 31, 2026
- huaweicloud/huaweicloud-skills
Open temporary SSH sessions to Huawei Cloud Ascend devices to monitor NPU health, manage disks/LVM, and troubleshoot, with in-memory-only credentials.
About
Provides temporary SSH remote connections to Ascend NPU servers for disk/LVM management, NPU monitoring, container management, and log analysis, with passwords held only in memory. A developer uses it to inspect and troubleshoot Ascend devices, with sensitive operations gated by confirmation.
- In-memory-only credentials, destroyed after session
- Confirmation required before delete/modify operations
Huawei Cloud Ascend Remote Connect by the numbers
- 68 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #666 of 1,042 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/huaweicloud/huaweicloud-skills --skill huawei-cloud-ascend-remote-connectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 68 |
|---|---|
| repo stars | ★ 19 |
| Last updated | July 31, 2026 |
| Repository | huaweicloud/huaweicloud-skills ↗ |
What it does
Open temporary SSH sessions to Huawei Cloud Ascend devices to monitor NPU health, manage disks/LVM, and troubleshoot, with in-memory-only credentials.
Files
Huawei Cloud Ascend Remote Connection
Overview
Provides temporary SSH remote connection capability for Huawei Cloud Ascend devices. Supports simultaneous connection to multiple machines. All sensitive operations (delete, modify, move, etc.) require user confirmation before execution. Passwords are only stored in memory and destroyed after session ends.
Architecture
System Architecture Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ User Interaction Layer │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Natural Language Commands / CLI Arguments │ │
│ │ (connect, execute, monitor, disconnect) │ │
│ └────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
├────────────────────────────────┼─────────────────────────────────────┤
│ Skill Core Components │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Session Manager │←→│ Command Executor│←→│ SSH Client │ │
│ │ - Connection │ │ - NL Parsing │ │ - paramiko │ │
│ │ - Pooling │ │ - Validation │ │ - ControlMaster│ │
│ │ - Timeout Mgmt │ │ - Execution │ │ - Key/Cert Auth│ │
│ └─────────────────┘ └─────────────────┘ └────────┬────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌─────────────────┐ │
│ │ │ │ Command Validator│ │
│ │ │ │ - Blocked Cmds │ │
│ │ │ │ - Confirm Req │ │
│ │ │ └─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
├────────────────────────────────┼─────────────────────────────────────┤
│ Huawei Cloud Ascend Infrastructure │
│ (Remote Target Servers) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Ascend NPU │ │ OS Services │ │ Containers │ │
│ │ - npu-smi │ │ - systemctl │ │ - docker │ │
│ │ - Driver │ │ - journalctl │ │ - k8s │ │
│ │ - FW Upgrade │ │ - Network Mgmt │ │ - Pods │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ Data Flow: User → Skill → SSH Tunnel → Target Server → Response │
└─────────────────────────────────────────────────────────────────────┘Component Relationships
| Component | Responsibility | Key Features |
|---|---|---|
| Session Manager | Manage SSH connections | Connection pooling, timeout management, lifecycle control |
| Command Executor | Process user commands | Natural language parsing, command routing, result formatting |
| SSH Client | Establish secure tunnels | paramiko integration, ControlMaster, authentication |
| Command Validator | Security enforcement | Blocked commands list, confirmation requirements |
Cloud Service Integration
- Ascend NPU Management: Direct access to npu-smi for monitoring and management
- Huawei Cloud Infrastructure: Secure SSH access to cloud servers and containers
- Security Compliance: Memory-only credential storage, session isolation
Prerequisites
System Requirements
- Python 3.8+
- paramiko >= 3.4.0
Environment Check
Prerequisite check: Python3 + paramiko required
```bash
python3 --version # Python3 >= 3.8
python3 -c "import paramiko; print('OK')" # SSH library
```
If not installed: pip3 install --user paramiko cryptographyAuthentication
Security rules (must be followed):
- Prohibited from reading, echoing, or printing password values
- Prohibited from asking the user to input passwords directly in the conversation
- Only allowed to read credentials from command line arguments
Parameter Confirmation
Input Parameters
| Parameter | Required | Description |
|---|---|---|
| host | Yes | Target server IP address |
| port | No | SSH port (default: 22) |
| user | Yes | SSH username |
| password | Yes | SSH password |
Parameter Validation
- IP address format: IPv4 (e.g., 192.168.1.100)
- Port range: 1-65535
- Username: alphanumeric and underscore
- Password: non-empty string
Confirmation Requirements
The following operations require explicit user confirmation:
- Delete: rm, rmdir
- Format: mkfs, fdisk -d, parted rm
- Unmount: umount
- Reboot: reboot, shutdown, init 6
- Shutdown: poweroff, halt, init 0
- User delete: userdel, groupdel
- Permission change: chmod -R, chown -R
IAM Permission Policies
Ensure the target server has SSH service enabled and the provided credentials have appropriate permissions.
Minimum required permissions on target server:
- SSH access (port 22 or custom)
- Sudo privileges for system management operations
Core Workflow
Task 1: Establish SSH Connection
python3 scripts/main.py --host <ascend-server-ip> --port 22 --user root --password <your-password> --command "npu-smi info"Task 2: Interactive Mode
python3 scripts/main.pyUsage Instructions
Connect to Ascend Server
SSH connect to 192.168.1.100 port 22 as root with password xxxExecute Commands
NPU Monitoring
npu-smi info
Check NPU status
NPU health checkSystem Management
Check CPU and memory
df -h
top -bn1Connection Management
Show current connections
Switch to 192.168.1.101
Disconnect SSH---
Output Format
Standard Response Format
All command outputs follow a structured format:
┌─────────────────────────────────────────┐
│ Target: <host>:<port> │
│ Command: <executed-command> │
│ Exit Code: <0-success/non-zero-fail> │
├─────────────────────────────────────────┤
│ STDOUT: │
│ <command-output> │
├─────────────────────────────────────────┤
│ STDERR: │
│ <error-output> │
├─────────────────────────────────────────┤
│ Duration: <seconds>s │
└─────────────────────────────────────────┘Error Response Format
[ERROR] <error-code>
Message: <error-description>
Suggestion: <troubleshooting-tip>Success Indicators
- Exit Code: 0
- STDOUT: Contains expected output
- STDERR: Empty or contains only warnings
---
Verification Method
Basic Verification Steps
1. Environment Check
python3 --version # Verify Python 3.8+
python3 -c "import paramiko" # Verify paramiko installed2. Connection Test
python3 scripts/main.py --host <test-ip> --port 22 --user root --password <test-pwd> --command "echo test"3. NPU Monitoring Test
python3 scripts/main.py --host <ascend-ip> --user root --password <pwd> --command "npu-smi info"Expected Results
| Test Case | Expected Output |
|---|---|
| Environment check | Python version >= 3.8, paramiko import success |
| Connection test | Exit code 0, "test" in stdout |
| NPU info | NPU device information displayed |
See references/verification-method.md for detailed verification procedures.
---
Script Files
Entry File
- main.py: Skill entry file (required)
- Function: Provide interactive menu, unified entry point
- Menu options:
- Establish SSH connection
- Execute commands
- Disconnect
Core Scripts
- executor.py: Command executor (main entry)
- Function: Parse user input, dispatch commands to corresponding handlers
- Core methods:
handle_command(text): Command dispatch entry_connect(info): Establish SSH connection_detect_disks(): Detect unmounted disks_handle_auto_mount(text): Configure auto-mount on boot_confirm_disk_merge(info): Disk merge confirmation
- session_manager.py: Session manager
- Function: Manage multiple concurrent SSH sessions
- Core methods:
create_session(host, port, username, password): Create new sessionexecute_command(command): Execute command in active sessionswitch_session(host): Switch to specified host sessionclose_session(): Close current sessionget_session_info(): Get all session information
- ssh_client.py: SSH client
- Function: Low-level SSH connection implementation, supports password and key authentication
- Core methods:
connect(): Establish SSH connectionexec_command(command): Execute remote commandclose(): Close connection
- command_validator.py: Command validator
- Function: Security layer, filter dangerous commands
- Validation rules:
- Blocklist: Direct block (e.g., fork bomb)
- Sensitive: Require confirmation (e.g., rm -rf, reboot)
- Allowlist: Direct execution (e.g., ls, cat, df)
---
Supported Features
NPU Management
- NPU status monitoring (npu-smi)
- NPU health check
- NPU configuration viewing
Disk Management
- Disk detection
- LVM merging
- Partition mounting
- Auto-mount on boot
- Disk health check
- Free space check
System Management
- CPU/Memory/Disk monitoring
- System updates
- User/permission management
- Cron job management
Network Management
- Port scanning
- Firewall configuration
- Route viewing
- Network interface configuration
- DNS troubleshooting
Container Management
- Docker installation and management
- Image management
- Container management
- Log viewing
- Docker Compose operations
Security Management
- Login auditing
- SSH key management
- High-risk command blocking
Log Management
- System logs
- Application log analysis
- Error troubleshooting
File Operations
- Upload/download
- Copy/transfer
- Change permissions
- Create/delete
---
Connection Pool Management
Features
- Connection Reuse: Reuse same connection for same target, avoid repeated handshakes
- Auto Disconnect: Auto disconnect after 10 minutes idle (configurable)
- Thread Safe: Support multi-thread concurrent access
- Status Query: View connection pool status and idle time
Protection Mechanisms
- Max Connections Limit: Default 50, supports multi-target machines
- Request Rate Limiting: Default max 200 concurrent requests
- Connection Timeout: Default 10 seconds
- Execute Timeout: Default 60 seconds
- Health Check: Check connection status every 30 seconds, auto disconnect bad connections
# Using connection pool
from ssh_client import get_pool
pool = get_pool()
result = pool.execute(conn_info, 'ls -la')
# View pool status (includes statistics)
status = pool.get_pool_status()
# {
# 'connections': [...],
# 'total_connections': 2,
# 'max_connections': 10,
# 'max_concurrent_requests': 5,
# 'statistics': {'total_requests': 100, 'failed_requests': 2, ...}
# }
# Configure protection parameters
pool.configure(
max_connections=20, # Max connections
max_concurrent_requests=10, # Max concurrent requests
idle_timeout=300, # Idle timeout (seconds)
connect_timeout=15, # Connection timeout (seconds)
execute_timeout=120 # Execute timeout (seconds)
)---
Multi-Machine Connection
Support simultaneous connection to multiple machines, distinguished by IP address:
SSH connect to 192.168.1.100 port 22 as root with password xxx
SSH connect to 192.168.1.101 port 2222 as admin with password yyySwitch Target
Switch to 192.168.1.101View Current Connections
View current connections---
Security Mechanisms
Sensitive Operation Confirmation
The following operations require user confirmation:
- Delete: rm, rmdir
- Format: mkfs, fdisk -d, parted rm
- Unmount: umount
- Reboot: reboot, shutdown, init 6
- Shutdown: poweroff, halt, init 0
- User delete: userdel, groupdel
- Permission change: chmod -R, chown -R
High-Risk Command Blocking
The following commands are blocked by default:
:(){ :|:& };:(fork bomb)- Direct disk formatting commands
Password Security
- Passwords only stored in memory
- Immediately cleared after session ends
- Not written to any configuration file or log
---
Best Practices
Connection Management
1. Reuse Connections: Connection pool automatically reuses existing connections 2. Timeout Settings: Adjust timeouts based on network conditions 3. Idle Timeout: Default 10 minutes; set shorter for frequent disconnects
Security Recommendations
1. Use Key Authentication: Prefer SSH keys over passwords when possible 2. Limit Permissions: Grant minimum sudo privileges needed 3. Monitor Sessions: Regularly check active connections 4. Log Auditing: Review login logs periodically
Performance Optimization
1. Batch Commands: Group related commands to reduce connection overhead 2. Connection Pool Tuning: Adjust pool size based on concurrent needs 3. Command Timeout: Set appropriate timeout values for long-running commands
---
Notes
Security Warnings
⚠️ Credential Handling:
- Never log or display passwords
- Clear credentials from memory after use
- Use SSH keys for production environments
⚠️ High-Risk Operations:
- Always confirm destructive operations (rm, mkfs, reboot)
- Blocked commands cannot be bypassed
- Review security mechanisms before deployment
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Connection timeout | Network/firewall | Check network connectivity |
| Authentication failed | Wrong credentials | Verify username/password |
| Command blocked | Security policy | Review command validator rules |
| Pool exhausted | Too many connections | Increase max_connections |
Limitations
- SSH password authentication only (no interactive password prompt)
- Single-user session per connection
- Maximum 50 concurrent connections (configurable)
---
Troubleshooting
Connection Failed
- Check if IP address and port are correct
- Check if firewall allows the port
- Check if username and password are correct
- Check if SSH service is running on target host
Command Execution Failed
- Check if command syntax is correct
- Check if there is an active connection
- Check if command is blocked by security policy
Disk Merge Failed
- Check if disk is occupied by other processes
- Check if LVM tools are installed
- Check if disk is already mounted
---
Directory Structure
huawei-cloud-ascend-remote-connect/
├── SKILL.md # Skill definition entry file (required)
├── scripts/ # Scripts directory (required)
│ ├── __init__.py # Module export
│ ├── main.py # Skill entry script (required)
│ ├── executor.py # Command executor
│ ├── session_manager.py # Session manager
│ ├── ssh_client.py # SSH client implementation
│ └── command_validator.py # Command validator
└── references/ # Reference documentation directory
├── troubleshooting.md # Troubleshooting guide
├── verification-method.md # Verification steps
└── iam-policies.md # IAM policies---
References
| Document | Description |
|---|---|
| references/troubleshooting.md | Troubleshooting guide |
| references/verification-method.md | Verification steps |
| scripts/main.py | Main entry script |
Author: huawei-cloud
IAM Permission Policy
Overview
This document describes the permission requirements for the Huawei Cloud Ascend Remote Connection skill.
Target Server Permissions
SSH Access Requirements
The user account used for SSH connection must have:
| Permission | Description |
|---|---|
| SSH login | Access to SSH service (port 22) |
| Command execution | Execute basic shell commands |
| sudo privileges | For system management operations |
Minimum Required Privileges
# Example sudoers configuration for non-root user
echo "ascend-user ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/ascend-userAllowed Commands
Safe Commands (No confirmation required)
npu-smi info- NPU status checkls,cat,head,tail- File operationsdf -h,free -h,uptime- System monitoringps,top- Process viewingss,netstat- Network information
Sensitive Commands (Require confirmation)
rm,rmdir- Delete operationsmkfs- Format operationsumount- Unmount operationsreboot,shutdown- System restart/shutdowndocker rm,docker rmi- Container/image removal
Blocked Commands (Always blocked)
- Fork bomb patterns
- Direct disk formatting without confirmation
Security Best Practices
1. Use Non-Root User
# Create dedicated user
useradd -m ascend-admin
usermod -aG sudo ascend-admin2. Restrict SSH Access
# Edit SSH configuration
sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#AllowUsers/AllowUsers/' /etc/ssh/sshd_config
echo "AllowUsers ascend-admin" >> /etc/ssh/sshd_config
systemctl restart sshd3. Use Key-Based Authentication (Recommended)
# On client machine
ssh-keygen -t ed25519
ssh-copy-id ascend-admin@<server-ip>
# On server - disable password authentication
sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd4. Firewall Configuration
# Allow only specific IPs
iptables -A INPUT -p tcp --dport 22 -s <trusted-ip> -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROPPermission Matrix
| Operation | Required Permission |
|---|---|
| Connect to server | SSH access |
| View NPU status | Regular user |
| View system info | Regular user |
| Disk management | sudo |
| System updates | sudo |
| Container management | sudo or docker group |
| User management | sudo |
Audit Logging
Enable SSH logging for security auditing:
# Configure SSH logging
echo "LogLevel VERBOSE" >> /etc/ssh/sshd_config
systemctl restart sshd
# Monitor logs
tail -f /var/log/auth.logTroubleshooting Guide
Overview
This document provides troubleshooting guidance for Huawei Cloud Ascend Remote Connection skill. It covers common connection issues, authentication problems, and command execution failures.
Connection Issues
Problem: Connection Failed
Possible Causes: 1. Target server IP address is incorrect 2. SSH port is not open (default 22) 3. Firewall blocking SSH access 4. SSH service not running on target server
Solution:
# Check if SSH port is accessible
telnet <host> <port>
# Check SSH service status on target server
systemctl status sshd
# Check firewall rules
iptables -L -n | grep 22Problem: Authentication Failed
Possible Causes: 1. Incorrect username or password 2. Password authentication disabled in SSH config 3. Key-based authentication required
Solution:
# Check SSH configuration on target server
grep -E "^(PasswordAuthentication|ChallengeResponseAuthentication)" /etc/ssh/sshd_config
# Enable password authentication if needed
sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl restart sshdCommand Execution Issues
Problem: Command Timeout
Possible Causes: 1. Command execution takes too long 2. Network latency 3. Target server unresponsive
Solution:
# Check network connectivity
ping <host>
# Try simpler commands first
python3 scripts/main.py --host <host> --command "echo test"Problem: Permission Denied
Possible Causes: 1. Insufficient permissions for the command 2. Need sudo privileges
Solution:
# Use sudo for privileged commands
python3 scripts/main.py --host <host> --command "sudo npu-smi info"NPU Monitoring Issues
Problem: npu-smi command not found
Possible Causes: 1. NPU driver not installed 2. npu-smi not in PATH
Solution:
# Check if NPU driver is installed
ls /usr/local/Ascend/driver
# Add to PATH
export PATH=$PATH:/usr/local/Ascend/driver/bin
npu-smi infoSecurity Issues
Problem: High-risk command blocked
Description: The skill blocks dangerous commands for security.
Solution:
- For sensitive operations like
rm -rf, confirm when prompted - For blocked commands (fork bomb), use alternative approaches
Logging
Enable debug logging:
python3 scripts/main.py --debugCheck connection pool status:
from ssh_client import get_pool
pool = get_pool()
print(pool.get_pool_status())Common Error Codes
| Error Code | Description | Solution |
|---|---|---|
| connection_failed | Cannot connect to target | Check network, firewall, SSH service |
| authentication_failed | Wrong credentials | Verify username/password |
| timeout | Command execution timeout | Check network latency, simplify command |
| permission_denied | Insufficient permissions | Use sudo or check user privileges |
| rate_limit | Too many concurrent requests | Wait and retry |
Verification Method
Overview
This document describes how to verify the Huawei Cloud Ascend Remote Connection skill is working correctly.
Prerequisites
1. Python 3.8+ installed 2. paramiko library installed 3. Target Ascend server with SSH enabled
Verification Steps
Step 1: Install Dependencies
pip3 install --user paramiko cryptographyStep 2: Basic Connection Test
# Test with command line arguments
python3 scripts/main.py --host <ascend-server-ip> --port 22 --user root --password <password> --command "echo 'Connection successful'"Expected Output:
Connection successfulStep 3: NPU Status Check
python3 scripts/main.py --host <ascend-server-ip> --user root --password <password> --command "npu-smi info"Expected Output:
- NPU device information
- Device status (Normal)
- Memory usage
- Temperature
Step 4: Interactive Mode Test
python3 scripts/main.pyThen enter:
SSH connect to <ascend-server-ip> port 22 as root with password <password>Expected: Connection established successfully
Step 5: System Command Test
After establishing connection:
Check CPU and memoryExpected Output:
- CPU information
- Memory usage (free -h)
Step 6: Sensitive Operation Confirmation Test
Delete /tmp/test.txtExpected Output:
⚠️⚠️ High-risk operation: Will delete /tmp/test.txt
Please reply "Confirm" or "Cancel"Step 7: Connection Pool Test
# Create test script
cat > /tmp/test_pool.py << 'EOF'
from scripts.ssh_client import get_pool, ConnectionInfo
pool = get_pool()
conn_info = ConnectionInfo(host='<host>', port=22, username='root', password='<password>')
# Test connection
result = pool.execute(conn_info, 'npu-smi info')
print(f"Success: {not result.error}")
print(f"Output: {result.stdout[:500]}")
# Check pool status
status = pool.get_pool_status()
print(f"Connections: {status['total_connections']}")
print(f"Max connections: {status['max_connections']}")
pool.close_all()
EOF
python3 /tmp/test_pool.pyAcceptance Criteria
Must Pass
1. ✅ Connection establishment via command line 2. ✅ Connection establishment via natural language 3. ✅ NPU status monitoring (npu-smi) 4. ✅ Basic system commands execution 5. ✅ Sensitive operation confirmation 6. ✅ Connection pool management 7. ✅ Multi-machine connection support
Security Checks
1. ✅ Password not printed in logs 2. ✅ High-risk commands blocked 3. ✅ Sensitive operations require confirmation 4. ✅ Password only stored in memory
Automated Testing
# Run basic verification
python3 -c "
from scripts.main import run_one_shot
result = run_one_shot('<host>', 22, 'root', '<password>', 'npu-smi info')
assert result == 0, 'NPU check failed'
print('All tests passed!')
"Troubleshooting Failed Tests
| Test | Failure Reason | Fix |
|---|---|---|
| Connection | SSH port not open | Check firewall, start sshd |
| NPU Check | npu-smi not found | Install NPU driver |
| Permission | No sudo access | Grant sudo privileges |
from .ssh_client import SSHClient, SSHResult, ConnectionInfo
from .session_manager import SessionManager
from .command_validator import CommandValidator, CommandType
from .executor import CommandExecutor
__all__ = [
'SSHClient',
'SSHResult',
'ConnectionInfo',
'SessionManager',
'CommandValidator',
'CommandType',
'CommandExecutor',
]
import re
from enum import Enum
from typing import Tuple, Optional, List
class CommandType(Enum):
ALLOWED = 'allowed'
CONFIRM_REQUIRED = 'confirm_required'
BLOCKED = 'blocked'
class CommandValidator:
def __init__(self):
self.sensitive_patterns: List[Tuple[str, str]] = [
(r'^\s*rm\s+(-[a-zA-Z]+)?\s+(-rf\b|\s+-r\s+-f\b|\s+-f\s+-r\b)', 'remove command'),
(r'^\s*rm\s+-[a-zA-Z]*[rf][a-zA-Z]*\s+/.+', 'recursive remove system path'),
(r'^\s*rmdir\s+/.+', 'remove system directory'),
(r'^\s*mkfs\b', 'format command'),
(r'^\s*fdisk\s+[-/]\s*[dD]', 'fdisk delete partition'),
(r'^\s*parted\s+.+\s+rm\s+', 'parted delete partition'),
(r'^\s*umount\b', 'unmount command'),
(r'^\s*reboot\b', 'reboot command'),
(r'^\s*shutdown\b', 'shutdown command'),
(r'^\s*init\s+[06]\b', 'system runlevel switch'),
(r'^\s*poweroff\b', 'power off'),
(r'^\s*halt\b', 'system halt'),
(r'^\s*userdel\b', 'delete user'),
(r'^\s*groupdel\b', 'delete group'),
(r'^\s*chmod\s+-R\b', 'recursive chmod'),
(r'^\s*chown\s+-R\b', 'recursive chown'),
(r'^\s*dd\s+if=\S+\s+of=\S+', 'dangerous write operation'),
]
self.blocked_patterns: List[Tuple[str, str]] = [
(r'^:\(\)\s*\{.*\|\|.*\s*\};\s*:', 'fork bomb attack'),
(r'^\s*mkfs\s+\S*/dev/sd\S+', 'direct disk format'),
(r'^\s*dd\s+if=/dev/zero\s+of=/dev/\S+', 'zero write to disk'),
(r'^\s*rm\s+-rf\s+/\s*$', 'delete root directory'),
(r'^\s*rm\s+-rf\s+/\b', 'delete system root path'),
]
self.allowed_prefixes: List[str] = [
'ls', 'pwd', 'cd', 'cat', 'grep', 'find', 'wc', 'head', 'tail',
'echo', 'date', 'who', 'w', 'last', 'top', 'htop', 'free', 'df',
'du', 'ps', 'netstat', 'ss', 'ping', 'traceroute', 'curl', 'wget',
'python', 'pip', 'node', 'npm', 'git', 'docker', 'docker-compose',
'systemctl status', 'journalctl', 'uname', 'hostname', 'ifconfig',
'ip addr', 'ip route', 'dig', 'nslookup', 'man', 'which', 'whereis',
'mkdir', 'touch', 'cp', 'mv', 'diff', 'patch', 'tar', 'zip',
'unzip', 'gzip', 'bzip2', 'xz', 'ssh', 'scp', 'sftp', 'rsync',
'crontab -l', 'passwd', 'su', 'sudo', 'apt', 'apt-get', 'yum',
'dnf', 'rpm', 'dpkg', 'service', 'systemctl', 'lvm', 'pvdisplay',
'vgdisplay', 'lvdisplay', 'fdisk -l', 'parted -l', 'smartctl',
'iostat', 'vmstat', 'sar', 'tcpdump', 'nc', 'openssl', 'ssh-keygen',
'usermod', 'groupmod', 'useradd', 'groupadd', 'cron', 'at',
'sysctl', 'ulimit', 'ufw', 'firewall-cmd', 'iptables', 'bridge',
'bond', 'vlan', 'route', 'netplan', 'nmcli', 'hostnamectl',
'timedatectl', 'loginctl', 'blkid', 'lsblk', 'mount', 'fstab',
'mkswap', 'swapon', 'swapoff', 'lsof', 'kill', 'killall', 'pkill',
]
def validate(self, command: str) -> Tuple[CommandType, Optional[str]]:
cmd = command.strip().lower()
for pattern, description in self.blocked_patterns:
if re.search(pattern, cmd):
return CommandType.BLOCKED, f"High-risk command blocked: {description}"
for pattern, description in self.sensitive_patterns:
if re.search(pattern, cmd):
return CommandType.CONFIRM_REQUIRED, f"Sensitive operation: {description}"
for prefix in self.allowed_prefixes:
if cmd.startswith(prefix):
return CommandType.ALLOWED, None
return CommandType.ALLOWED, None
def is_sensitive(self, command: str) -> bool:
result, _ = self.validate(command)
return result == CommandType.CONFIRM_REQUIRED
def is_blocked(self, command: str) -> bool:
result, _ = self.validate(command)
return result == CommandType.BLOCKED
def get_validation_message(self, command: str) -> Optional[str]:
result, message = self.validate(command)
if result == CommandType.BLOCKED:
return f"Command blocked: {message}"
if result == CommandType.CONFIRM_REQUIRED:
return f"Requires confirmation: {message}"
return None
import json
import re
from typing import Optional, Dict, Any, List, Tuple
from .session_manager import SessionManager
from .command_validator import CommandValidator, CommandType
class CommandExecutor:
def __init__(self):
self.session_manager = SessionManager()
self.command_validator = CommandValidator()
self.pending_confirmation = None
self.pending_disk_merge = None
# ========== Connection Parsing ==========
def parse_connection_string(self, text: str) -> Optional[Dict[str, Any]]:
patterns = {
'host': r'(?:connect|address|IP|host)\s*[::]?\s*(\d{1,3}(?:\.\d{1,3}){3})',
'port': r'(?:port|port)\s*[::]?\s*(\d+)',
'user': r'(?:account|username|user|root|admin)\s*[::]?\s*(\w+)',
'password': r'(?:password|pwd|pass)\s*[::]?\s*(\S+)',
}
result = {}
for key, pattern in patterns.items():
match = re.search(pattern, text, re.IGNORECASE)
if match:
result[key] = match.group(1)
if 'host' in result:
result.setdefault('port', '22')
result.setdefault('user', 'root')
return result
return None
# ========== Main Entry ==========
def handle_command(self, text: str) -> str:
text = text.strip()
# Cancel/Confirm
if text in ('cancel', 'abort'):
self.pending_confirmation = None
self.pending_disk_merge = None
return 'Operation cancelled'
if text == 'confirm' and self.pending_confirmation:
command = self.pending_confirmation
self.pending_confirmation = None
return self._validate_and_execute(command)
if text == 'confirm' and self.pending_disk_merge:
return self._execute_disk_merge(self.pending_disk_merge)
# Connection Management
if text == 'view connections':
return self._list_connections()
if text in ('disconnect SSH', 'disconnect', 'exit SSH'):
return self._disconnect()
# ===== Natural Language Module Routing =====
# Match modules by priority
# Disk Module
r = self._handle_disk_nl(text)
if r:
return r
# System Monitoring Module
r = self._handle_system_nl(text)
if r:
return r
# Network Module
r = self._handle_network_nl(text)
if r:
return r
# Docker/Container Module
r = self._handle_docker_nl(text)
if r:
return r
# Security Module
r = self._handle_security_nl(text)
if r:
return r
# Log Module
r = self._handle_log_nl(text)
if r:
return r
# File Operation Module
r = self._handle_file_nl(text)
if r:
return r
# connect
connect_info = self.parse_connection_string(text)
if connect_info:
return self._connect(connect_info)
# Execute directly if connected
if self.session_manager.active_session:
return self._validate_and_execute(text)
return 'Please provide target host IP, port, username and password'
# ========== Disk Module ==========
def _handle_disk_nl(self, text: str) -> Optional[str]:
"""Disk-related natural language"""
keywords_map = {
# detect/view
('detect disk', 'view disk', 'unmounted disk', 'disk status', 'disk list', 'lsblk'): 'detect',
('disk health', 'disk SMART', 'smartctl', 'disk check'): 'health',
('free space', 'available space', 'disk free', 'space check'): 'space',
('disk space', 'disk detail', 'disk full', 'disk comprehensive'): 'full_report',
# LVM
('LVM information', 'volume group info', 'VG info', 'LV info', 'logical volume'): 'lvm_info',
('LVM extend', 'LV extend', 'logical volume extend', 'extend'): 'lvm_extend',
# Partition
('partition information', 'view partition', 'fdisk'): 'partition_info',
# mounted at/unmount
('auto mount on boot', 'boot mount', 'fstab', 'auto mount'): 'auto_mount',
('unmount', 'umount'): 'umount',
}
for keywords, action in keywords_map.items():
if any(kw in text for kw in keywords):
if not self.session_manager.active_session:
return 'Please establish SSH connection first'
if action == 'detect':
return self._detect_disks()
elif action == 'full_report':
return self._disk_full_report()
elif action == 'health':
return self._exec_simple('disk health check', 'smartctl -a /dev/sda 2>/dev/null || echo "smartctl not installed, trying lsblk"; lsblk -o NAME,SIZE,TYPE,ROTA,MOUNTPOINT')
elif action == 'space':
return self._exec_simple('disk free space', 'df -h | grep -v tmpfs | grep -v devtmpfs | grep -v overlay | grep -v shm')
elif action == 'lvm_info':
return self._exec_simple('LVM information', 'echo "=== volume group ==="; vgdisplay 2>/dev/null | grep -E "VG Name|VG Size|Free PE|Alloc PE"; echo; echo "=== logical volume ==="; lvdisplay 2>/dev/null | grep -E "LV Name|LV Size|VG Name"; echo; echo "=== physical volume ==="; pvdisplay 2>/dev/null | grep -E "PV Name|PV Size|VG Name"')
elif action == 'lvm_extend':
return self._handle_lvm_extend(text)
elif action == 'partition_info':
return self._exec_simple('partition information', 'fdisk -l 2>/dev/null || parted -l 2>/dev/null')
elif action == 'auto_mount':
return self._handle_auto_mount(text)
elif action == 'umount':
return self._handle_umount(text)
# Disk merge
if any(kw in text for kw in ('merge', 'LVM merge', 'Disk merge')):
merge_info = self.parse_disk_merge_command(text)
if merge_info:
if not self.session_manager.active_session:
return 'Please establish SSH connection first'
return self._confirm_disk_merge(merge_info)
return '❌ Unable to parseDisk mergecommand. Please input in format:\nExample: Merge nvme0n1, nvme1n1, nvme2n1merged and mounted to/home'
# mounted at(not auto mount on boot)
mount_match = re.search(r'mounted at\s+(\S+)\s*to\s*(\S+)', text)
if mount_match:
if not self.session_manager.active_session:
return 'Please establish SSH connection first'
device, mnt = mount_match.group(1), mount_match.group(2)
self.pending_confirmation = f'mkdir -p {mnt} && mount {device} {mnt}'
return f'⚠️ Will mount {device} to {mnt},confirm to execute?\n\nPlease reply "confirm" or "cancel"'
return None
def _handle_lvm_extend(self, text: str) -> str:
"""Handle LVM extend"""
# parse:extend /dev/vg_home/lv_home increase by 100G
lv_match = re.search(r'extend\s+(\S+)\s*(?:increase by|increase by|extend by)\s*(\d+)\s*[GTgt]', text)
if lv_match:
lv_path = lv_match.group(1)
size = lv_match.group(2)
self.pending_confirmation = f'lvextend -L +{size}G {lv_path} && resize2fs {lv_path}'
return f'⚠️ Will extend logical volume {lv_path} increase by {size}G,confirm to execute?\n\nPlease reply "confirm" or "cancel"'
# parse:extend /home increase by 100G
mnt_match = re.search(r'extend\s+(\S+)\s*(?:increase by|increase by|extend by)\s*(\d+)\s*[GTgt]', text)
if mnt_match:
mnt = mnt_match.group(1)
size = mnt_match.group(2)
self.pending_confirmation = f'lvextend -L +{size}G $(df --output=source {mnt} | tail -1) && resize2fs $(df --output=source {mnt} | tail -1)'
return f'⚠️ Will extend mount point {mnt} increase by {size}G,confirm to execute?\n\nPlease reply "confirm" or "cancel"'
return '❌ Unable to parse extend command\n\nExample: extend /dev/vg_home/lv_home increase by 100G\n extend /home increase by 50G'
def _handle_umount(self, text: str) -> str:
"""Handle unmount"""
target = re.search(r'unmount\s+(\S+)', text)
if target:
path = target.group(1)
self.pending_confirmation = f'umount {path}'
return f'⚠️ Sensitive: Will unmount {path}\n\nPlease reply "confirm" or "cancel"'
return '❌ Please specify unmount target,Example: unmount /home'
# ========== System Monitoring Module ==========
def _handle_system_nl(self, text: str) -> Optional[str]:
"""system monitoringnatural language"""
if not self.session_manager.active_session:
# Only help info does not require connection
if any(kw in text for kw in ('system monitoring', 'system info', 'help', 'help')):
return self._system_help()
return None
keywords_map = {
('CPU info', 'CPU monitoring', 'CPU usage', 'CPU usage', 'cpu'): 'cpu',
('memory info', 'memory monitoring', 'memory usage', 'memory usage', 'memory'): 'memory',
('system info', 'system overview', 'system status', 'system monitoring', 'overview'): 'overview',
('system update', 'update system', 'upgrade', 'update'): 'update',
('uptime', 'uptime', 'start time'): 'uptime',
('user list', 'online users', 'logged-in users', 'who'): 'users',
('scheduled tasks', 'crontab', 'scheduled tasks', 'cron'): 'crontab',
('process list', 'process view', 'ps', 'top processes'): 'processes',
('environment variables', 'env'): 'env',
('system version', 'version info', 'OS version'): 'version',
('hostname', 'hostname'): 'hostname',
}
for keywords, action in keywords_map.items():
if any(kw in text for kw in keywords):
if action == 'cpu':
return self._exec_simple('CPU info', 'echo "=== CPU model ==="; lscpu | grep "Model name"; echo; echo "=== CPU Cores ==="; nproc; echo; echo "=== CPU Load ==="; uptime; echo; echo "=== TOP5 Processes ==="; ps -eo pid,comm,%cpu --sort=-%cpu | head -6')
elif action == 'memory':
return self._exec_simple('memory info', 'free -h; echo; echo "=== TOP5 memory processes ==="; ps -eo pid,comm,%mem,rss --sort=-%mem | head -6')
elif action == 'overview':
return self._exec_simple('system overview', 'echo "=== host ==="; hostname; echo; echo "=== System ==="; cat /etc/os-release | head -2; echo; echo "=== Kernel ==="; uname -r; echo; echo "=== uptime ==="; uptime; echo; echo "=== CPU ==="; lscpu | grep "Model name"; echo "Cores: $(nproc)"; echo; echo "=== Memory ==="; free -h | head -2; echo; echo "=== Disk ==="; df -h | grep -v tmpfs | grep -v devtmpfs | grep -v overlay | grep -v shm')
elif action == 'update':
self.pending_confirmation = 'apt-get update && apt-get upgrade -y 2>/dev/null || yum update -y 2>/dev/null || dnf upgrade -y 2>/dev/null || echo "Package manager not found"'
return '⚠️ Sensitive: system update\n\nWill execute system package update, may affect service operation.\n\nPlease reply "confirm" or "cancel"'
elif action == 'uptime':
return self._exec_simple('uptime', 'uptime')
elif action == 'users':
return self._exec_simple('online users', 'who; echo; echo "=== recent logins ==="; last -5')
elif action == 'crontab':
return self._exec_simple('scheduled tasks', 'echo "=== root crontab ==="; crontab -l 2>/dev/null || echo "none"; echo; for user in $(cut -d: -f1 /etc/passwd | head -10); do crontab -u $user -l 2>/dev/null && echo "[$user]:" && crontab -u $user -l 2>/dev/null; done 2>/dev/null | head -50')
elif action == 'processes':
return self._exec_simple('process list', 'ps aux --sort=-%cpu | head -20')
elif action == 'env':
return self._exec_simple('environment variables', 'env | sort | head -40')
elif action == 'version':
return self._exec_simple('system version', 'cat /etc/os-release; echo; uname -a')
elif action == 'hostname':
return self._exec_simple('hostname', 'hostname; hostnamectl 2>/dev/null')
return None
def _system_help(self) -> str:
return """📋 system monitoringcommands:
- CPU info / CPU usage
- memory info / memory usage
- system overview / system status
- uptime
- user list / online users
- scheduled tasks / crontab
- process list
- system update(requires confirmation)
- system version / hostname"""
# ========== Network Module ==========
def _handle_network_nl(self, text: str) -> Optional[str]:
"""Network-relatednatural language"""
if not self.session_manager.active_session:
return None
keywords_map = {
('port scan', 'port list', 'listening ports', 'open ports'): 'ports',
('firewall status', 'firewall', 'firewall', 'iptables', 'ufw'): 'firewall',
('routing table', 'route', 'route'): 'route',
('network interface', 'network interface', 'network interface', 'ip addr'): 'nic',
('DNS', 'dns', 'Dns', 'domain name parse'): 'dns',
('network connections', 'connection status', 'netstat', 'ss'): 'connections',
('network test', 'ping'): 'ping',
}
for keywords, action in keywords_map.items():
if any(kw in text for kw in keywords):
if action == 'ports':
return self._exec_simple('listening ports', 'ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null')
elif action == 'firewall':
return self._exec_simple('firewall status', 'echo "=== iptables ==="; iptables -L -n 2>/dev/null || echo "no permission or not installed"; echo; echo "=== ufw ==="; ufw status 2>/dev/null || echo "ufw not installed"; echo; echo "=== firewalld ==="; firewall-cmd --list-all 2>/dev/null || echo "firewalld not running"')
elif action == 'route':
return self._exec_simple('routing table', 'ip route show 2>/dev/null || route -n 2>/dev/null')
elif action == 'nic':
return self._exec_simple('network interface', 'ip addr show 2>/dev/null || ifconfig 2>/dev/null')
elif action == 'dns':
return self._exec_simple('DNS info', 'echo "=== resolv.conf ==="; cat /etc/resolv.conf; echo; echo "=== DNS Test ==="; nslookup google.com 2>/dev/null || dig google.com 2>/dev/null || echo "DNS tools not installed"')
elif action == 'connections':
return self._exec_simple('network connections', 'ss -tnp 2>/dev/null | head -30 || netstat -tnp 2>/dev/null | head -30')
elif action == 'ping':
host = re.search(r'ping\s+(\S+)', text)
target = host.group(1) if host else 'baidu.com'
return self._exec_simple(f'Ping {target}', f'ping -c 4 {target}')
# firewall operations (sensitive)
if any(kw in text for kw in ('open ports', 'open port', 'add port')):
port_match = re.search(r'port\s*(\d+)', text)
if port_match:
port = port_match.group(1)
self.pending_confirmation = f'iptables -A INPUT -p tcp --dport {port} -j ACCEPT 2>/dev/null; firewall-cmd --add-port={port}/tcp --permanent 2>/dev/null; firewall-cmd --reload 2>/dev/null; ufw allow {port} 2>/dev/null; echo "alreadytrytestopen ports {port}"'
return f'⚠️ Sensitive: open ports {port}\n\nPlease reply "confirm" or "cancel"'
return None
# ========== Docker/Container Module ==========
def _handle_docker_nl(self, text: str) -> Optional[str]:
"""Docker containernatural language"""
if not self.session_manager.active_session:
return None
# First check if it is a container-related command
docker_keywords = ('container', 'docker', 'Docker', 'image', 'compose', 'Compose')
if not any(kw in text for kw in docker_keywords):
return None
keywords_map = {
('container list', 'container status', 'docker ps', 'running containers'): 'ps',
('all containers', 'all containers', 'all containers'): 'ps_all',
('image list', 'docker images', 'image view'): 'images',
('container logs', 'dockerlog', 'logview'): 'logs',
('docker info', 'docker info', 'docker status', 'docker status', 'docker info'): 'info',
('docker disk', 'docker disk', 'docker size', 'docker size', 'docker usage', 'docker usage'): 'disk_usage',
('compose status', 'compose ps'): 'compose_ps',
}
for keywords, action in keywords_map.items():
if any(kw in text for kw in keywords):
if action == 'ps':
return self._exec_simple('running containers', 'docker ps --format "table {{.Names}}\\t{{.Status}}\\t{{.Image}}\\t{{.Ports}}"')
elif action == 'ps_all':
return self._exec_simple('all containers', 'docker ps -a --format "table {{.Names}}\\t{{.Status}}\\t{{.Image}}"')
elif action == 'images':
return self._exec_simple('docker images', 'docker images --format "table {{.Repository}}\\t{{.Tag}}\\t{{.Size}}\\t{{.CreatedSince}}"')
elif action == 'info':
return self._exec_simple('docker info', 'docker info 2>/dev/null | head -20')
elif action == 'disk_usage':
return self._exec_simple('Docker Disk Usage', 'docker system df')
elif action == 'compose_ps':
return self._exec_simple('compose status', 'docker compose ps 2>/dev/null || docker-compose ps 2>/dev/null || echo "no composeoseitemtarget"')
# container logs(requires container name)
log_match = re.search(r'(?:container|docker)?\s*log\s+(\S+)', text)
if not log_match:
log_match = re.search(r"(\S+)\s*(?:'s)?log", text)
if log_match:
container = log_match.group(1)
lines_match = re.search(r'(\d+)\s*lines', text)
lines = lines_match.group(1) if lines_match else '50'
return self._exec_simple(f'{container}log', f'docker logs --tail {lines} {container} 2>&1')
# containerstart/stop(sensitive)
start_match = re.search(r'start\s+(?:container\s+)?(\S+)', text)
if start_match:
container = start_match.group(1)
self.pending_confirmation = f'docker start {container}'
return f'⚠️ Will start container {container}\n\nPlease reply "confirm" or "cancel"'
stop_match = re.search(r'stop\s+(?:container\s+)?(\S+)', text)
if not stop_match:
stop_match = re.search(r'(?:container\s+)?(\S+)\s*stop', text)
if stop_match:
container = stop_match.group(1)
self.pending_confirmation = f'docker stop {container}'
return f'⚠️ Sensitive: Will stop container {container}\n\nPlease reply "confirm" or "cancel"'
restart_match = re.search(r'restart\s+(?:container\s+)?(\S+)', text)
if restart_match:
container = restart_match.group(1)
self.pending_confirmation = f'docker restart {container}'
return f'⚠️ Sensitive: Will restart container {container}\n\nPlease reply "confirm" or "cancel"'
# remove container(high-risk)
rm_match = re.search(r'delete\s+(?:container\s+)?(\S+)', text)
if rm_match:
container = rm_match.group(1)
self.pending_confirmation = f'docker rm -f {container}'
return f'⚠️⚠️ High-risk: Will remove container {container}\n\nPlease reply "confirm" or "cancel"'
# delete image(high-risk)
rmi_match = re.search(r'delete\s+(?:image\s+)?(\S+:\S+|\S+)', text)
if rmi_match and 'image' in text:
image = rmi_match.group(1)
self.pending_confirmation = f'docker rmi {image}'
return f'⚠️⚠️ High-risk: Will delete image {image}\n\nPlease reply "confirm" or "cancel"'
# Docker cleanup(high-risk)
if any(kw in text for kw in ('docker cleanup', 'docker cleanup', 'docker prune', 'docker clean')):
self.pending_confirmation = 'docker system prune -af 2>/dev/null'
return '⚠️⚠️ High-risk: Will clean up all unused Docker resources(containers, images, networks, cache)\n\nPlease reply "confirm" or "cancel"'
return None
# ========== Security Module ==========
def _handle_security_nl(self, text: str) -> Optional[str]:
"""Security-relatednatural language"""
if not self.session_manager.active_session:
return None
keywords_map = {
('login audit', 'login records', 'recent logins', 'last'): 'login_audit',
('failed logins', 'login failures', 'brute force', 'btmp'): 'failed_login',
('SSH config', 'SSH config', 'sshd config'): 'ssh_config',
('key list', 'SSH keys', 'authorized_keys'): 'ssh_keys',
('security check', 'security audit', 'security status'): 'security_check',
('user list', 'system users'): 'system_users',
('SUID files', 'suid', 'privileged files'): 'suid_check',
}
for keywords, action in keywords_map.items():
if any(kw in text for kw in keywords):
if action == 'login_audit':
return self._exec_simple('login audit', 'echo "=== recent successful logins ==="; last -20; echo; echo "=== Current Users ==="; who')
elif action == 'failed_login':
return self._exec_simple('failed logins', 'lastb -20 2>/dev/null || echo "No failed login records or permission denied"')
elif action == 'ssh_config':
return self._exec_simple('SSH config', 'grep -v "^#" /etc/ssh/sshd_config 2>/dev/null | grep -v "^$"')
elif action == 'ssh_keys':
return self._exec_simple('SSH keys', 'echo "=== root authorized_keys ==="; cat /root/.ssh/authorized_keys 2>/dev/null || echo "none"; for user in $(ls /home/ 2>/dev/null | head -5); do echo; echo "=== $user ==="; cat /home/$user/.ssh/authorized_keys 2>/dev/null || echo "none"; done')
elif action == 'security_check':
return self._exec_simple('security check', 'echo "=== open ports ==="; ss -tlnp 2>/dev/null | head -20; echo; echo "=== firewall ==="; iptables -L -n 2>/dev/null | head -10 || echo "none"; echo; echo "=== SSH config ==="; grep -E "^(PermitRootLogin|PasswordAuthentication|Port)" /etc/ssh/sshd_config 2>/dev/null; echo; echo "=== failed logins (recent 5) ==="; lastb -10 2>/dev/null | head -5 || echo "none"')
elif action == 'system_users':
return self._exec_simple('system users', 'cat /etc/passwd | grep -v nologin | grep -v false | grep -v sync')
elif action == 'suid_check':
return self._exec_simple('SUID files', 'find / -perm -4000 -type f 2>/dev/null | head -30')
# Key generation (sensitive)
if 'generate key' in text or 'create key' in text:
self.pending_confirmation = 'ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N "" && cat /root/.ssh/id_ed25519.pub'
return '⚠️ Will generate new ED25519 key pair\n\nPlease reply "confirm" or "cancel"'
return None
# ========== Log Module ==========
def _handle_log_nl(self, text: str) -> Optional[str]:
"""Log-relatednatural language"""
if not self.session_manager.active_session:
return None
keywords_map = {
('system logs', 'syslog', 'messages'): 'syslog',
('kernel log', 'dmesg', 'kernellog'): 'dmesg',
('SSH logs', 'sshlog', 'authlog'): 'auth_log',
('error log', 'errorlog', 'troubleshoot'): 'errors',
}
for keywords, action in keywords_map.items():
if any(kw in text for kw in keywords):
if action == 'syslog':
return self._exec_simple('system logs', 'tail -50 /var/log/syslog 2>/dev/null || tail -50 /var/log/messages 2>/dev/null || echo "logfilenotfindto"')
elif action == 'dmesg':
return self._exec_simple('kernel log', 'dmesg | tail -30')
elif action == 'auth_log':
return self._exec_simple('SSH logs', 'tail -30 /var/log/auth.log 2>/dev/null || tail -30 /var/log/secure 2>/dev/null || echo "logfilenotfindto"')
elif action == 'errors':
return self._exec_simple('troubleshoot', 'echo "=== system errors ==="; journalctl -p err --no-pager -n 20 2>/dev/null || grep -i error /var/log/syslog 2>/dev/null | tail -20 || grep -i error /var/log/messages 2>/dev/null | tail -20 || echo "noneerror log"')
# Specify log file
log_match = re.search(r'(?:view|read|analyze)?\s*log\s+(\S+)', text)
if log_match:
logfile = log_match.group(1)
lines_match = re.search(r'(\d+)\s*lines', text)
lines = lines_match.group(1) if lines_match else '50'
return self._exec_simple(f'log {logfile}', f'tail -{lines} {logfile} 2>/dev/null || echo "file does not exist"')
# journalctl
if 'journalctl' in text or 'service log' in text:
service = re.search(r'(?:service log|journalctl)\s+(\S+)', text)
if service:
return self._exec_simple(f'{service.group(1)}service log', f'journalctl -u {service.group(1)} --no-pager -n 50 2>/dev/null || echo "service not found"')
return self._exec_simple('system logs(journal)', 'journalctl --no-pager -n 30')
return None
# ========== File Operation Module ==========
def _handle_file_nl(self, text: str) -> Optional[str]:
"""file operationnatural language"""
if not self.session_manager.active_session:
return None
# view file/directory
if any(kw in text for kw in ('view file', 'read file', 'cat file')):
path = re.search(r'(?:view|read|cat)\s+(?:file\s+)?(\S+)', text)
if path:
return self._exec_simple(f'view {path.group(1)}', f'cat {path.group(1)} 2>/dev/null || echo "file does not exist"')
# list directory
ls_match = re.search(r'(?:list|ls|view)\s*(?:directory\s+)?(\S+)', text)
if ls_match and ('list' in text or 'ls' in text or 'directory' in text):
path = ls_match.group(1)
return self._exec_simple(f'list {path}', f'ls -lah {path} 2>/dev/null || echo "path does not exist"')
# findfile
find_match = re.search(r'find\s+(\S+)', text)
if find_match:
name = find_match.group(1)
return self._exec_simple(f'find {name}', f'find / -name "{name}" -type f 2>/dev/null | head -20')
# createdirectory
mkdir_match = re.search(r'(?:create|create)\s*(?:directory|folder)\s+(\S+)', text)
if mkdir_match:
path = mkdir_match.group(1)
self.pending_confirmation = f'mkdir -p {path}'
return f'⚠️ Will create directory {path}\n\nPlease reply "confirm" or "cancel"'
# create file
touch_match = re.search(r'(?:create|create)\s*(?:file)\s+(\S+)', text)
if touch_match:
path = touch_match.group(1)
self.pending_confirmation = f'touch {path}'
return f'Will create file {path},confirm?\n\nPlease reply "confirm" or "cancel"'
# delete file/directory(high-risk)
rm_match = re.search(r'delete\s*(?:file|directory|folder)?\s+(\S+)', text)
if rm_match and 'container' not in text and 'image' not in text:
path = rm_match.group(1)
self.pending_confirmation = f'rm -rf {path}'
return f'⚠️⚠️ High-risk: Will delete {path}\n\nPlease reply "confirm" or "cancel"'
# copyfile
cp_match = re.search(r'copy\s+(\S+)\s*(?:to|→|->)\s*(\S+)', text)
if cp_match:
src, dst = cp_match.group(1), cp_match.group(2)
self.pending_confirmation = f'cp -r {src} {dst}'
return f'⚠️ Will copy {src} → {dst}\n\nPlease reply "confirm" or "cancel"'
# movefile
mv_match = re.search(r'(?:move|move)\s+(\S+)\s*(?:to|→|->)\s*(\S+)', text)
if mv_match:
src, dst = mv_match.group(1), mv_match.group(2)
self.pending_confirmation = f'mv {src} {dst}'
return f'⚠️ Sensitive: Will move {src} → {dst}\n\nPlease reply "confirm" or "cancel"'
# chmod
chmod_match = re.search(r'(?:chmod|chmod)\s+(\S+)\s+(\S+)', text)
if chmod_match:
perm, path = chmod_match.group(1), chmod_match.group(2)
self.pending_confirmation = f'chmod {perm} {path}'
return f'⚠️ Sensitive: Will change {path} permissions to {perm}\n\nPlease reply "confirm" or "cancel"'
# file size
du_match = re.search(r'(?:file size|directory size|du)\s+(\S+)', text)
if du_match:
path = du_match.group(1)
return self._exec_simple(f'{path} size', f'du -sh {path} 2>/dev/null || echo "path does not exist"')
return None
# ========== General Execution ==========
def _exec_simple(self, title: str, command: str) -> str:
"""Simple execution with formatted output"""
result = self.session_manager.execute_command(command, timeout=60)
if result.error:
return f"❌ {title} Execution failed: {result.message}"
output = f"📋 {title}\n"
output += f"host: {result.target_host} | time: {result.duration:.2f}s\n"
output += f"{'─' * 40}\n"
if result.stdout:
output += result.stdout
if result.stderr:
output += f"\n⚠️ {result.stderr}"
return output
# ========== Original Disk Functions ==========
def parse_disk_merge_command(self, text: str) -> Optional[Dict[str, Any]]:
disk_pattern = r'(nvme\d+n\d+)'
disks = re.findall(disk_pattern, text)
if not disks:
return None
mount_point = None
for pattern in [r'mounted at\s*([/\w]+)', r'mounted at\s*([/\w]+)', r'to\s*([/\w]+)']:
match = re.search(pattern, text)
if match:
mount_point = match.group(1)
if not mount_point.startswith('/'):
mount_point = '/' + mount_point
break
if not mount_point:
mount_point = '/home'
return {'disks': disks, 'mount_point': mount_point}
def _disk_full_report(self) -> str:
"""Disk full report:Physical Disks + Partition Usage + LVM Combined Volumes + Docker Directory Mapping(table format)"""
if not self.session_manager.active_session:
return 'Please establish SSH connection first'
# Single command to reduce SSH round trips
cmd = r'''echo "=====Physical Disks====="
lsblk -d -o NAME,SIZE,TYPE,ROTA,MOUNTPOINT 2>/dev/null | grep -v "loop" | grep -v "ram"
echo
echo "=====Partition Usage====="
df -h | grep -v tmpfs | grep -v devtmpfs | grep -v overlay | grep -v shm
echo
echo "=====LVM Combined Volumes====="
if command -v vgdisplay &>/dev/null; then
echo "---volume group---"
vgdisplay 2>/dev/null | grep -E "VG Name|VG Size|Free PE|Alloc PE"
echo "---logical volume---"
lvdisplay 2>/dev/null | grep -E "LV Name|LV Size|VG Name"
echo "---physical volume---"
pvdisplay 2>/dev/null | grep -E "PV Name|PV Size|VG Name"
else
echo "LVM not installed"
fi
echo
echo "=====Docker Directory Mapping====="
if command -v docker &>/dev/null; then
docker ps --format '{{.Names}}' 2>/dev/null | while read cname; do
echo "-- $cname --"
docker inspect "$cname" --format '{{range .Mounts}} {{.Type}}: {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' 2>/dev/null
done
echo
echo "---Docker Disk Usage---"
docker system df 2>/dev/null
else
echo "Docker not installed"
fi'''
result = self.session_manager.execute_command(cmd, timeout=60)
if result.error:
return f"❌ Disk full reportExecution failed: {result.message}"
raw = result.stdout or ''
sections = {}
current = None
for line in raw.split('\n'):
if line.startswith('=====') and line.endswith('====='):
current = line.strip('=').strip()
sections[current] = []
elif current is not None:
sections[current].append(line)
output = f"📊 Disk full report\nhost: {result.target_host} | time: {result.duration:.2f}s\n{'─'*50}\n"
# ── Physical Disks (table with usage info) ──
output += "\n📦 Physical Disks\n"
# Collect partition usage info first,indexed by disk name or VG name
part_info = {}
if 'Partition Usage' in sections:
for l in sections['Partition Usage']:
parts = l.split()
if len(parts) >= 6 and parts[0] != 'Filesystem':
fs = parts[0]
mount = parts[5] if len(parts) > 5 else parts[-1]
if '/dev/mapper/' in fs:
vg_name = fs.replace('/dev/mapper/', '').split('-')[0]
part_info[vg_name] = (parts[1], parts[2], parts[3], parts[4], mount)
elif '/dev/' in fs:
disk_name = fs.replace('/dev/', '')
base = re.sub(r'p?\d+$', '', disk_name) if not disk_name.endswith('n1') else disk_name
if base not in part_info:
part_info[base] = (parts[1], parts[2], parts[3], parts[4], mount)
else:
old = part_info[base]
part_info[base] = (old[0], old[1], old[2], old[3], old[4] + ', ' + mount)
if 'Physical Disks' in sections:
lines = [l for l in sections['Physical Disks'] if l.strip()]
disks = []
for l in lines:
parts = l.split()
if len(parts) >= 3 and parts[0] != 'NAME':
name = '/dev/' + parts[0]
size = parts[1]
rota = parts[3] if len(parts) > 3 else '?'
disk_type = 'HDD' if rota == '1' else 'NVMe'
disks.append((name, size, disk_type, parts[0]))
if disks:
# detectLVM PV→VG mapping
lvm_pvs = {}
if 'LVM Combined Volumes' in sections:
pv_name = ''
for l in sections['LVM Combined Volumes']:
if 'PV Name' in l and '/dev/' in l:
pv_name = l.split('/dev/')[-1].strip().split()[0]
elif 'VG Name' in l and pv_name:
vg = l.split('VG Name')[-1].strip()
lvm_pvs[pv_name] = vg
pv_name = ''
# Find system disk
root_disk = None
for l in lines:
parts = l.split()
if len(parts) >= 2 and parts[0] != 'NAME':
if parts[0].startswith('sd') and not any(c.isdigit() for c in parts[0]):
root_disk = '/dev/' + parts[0]
break
# build row:(Disk, Size, Type, Usage, Total, Used, Available, Usage%)
rows = []
for d in disks:
dev_short = d[3]
if d[0] == root_disk:
use = 'System Disk'
info = part_info.get(dev_short, ('', '', '', '', ''))
elif dev_short in lvm_pvs:
vg = lvm_pvs[dev_short]
use = f'LVM→{vg}'
info = part_info.get(vg, ('', '', '', '', ''))
else:
use = 'Unallocated'
info = ('', '', '', '', '')
rows.append((d[0], d[1], d[2], use, info[0], info[1], info[2], info[3]))
if rows:
col_names = ['Disk', 'Size', 'Type', 'Usage', 'Total', 'Used', 'Available', 'Usage%']
col_vals = [[r[i] for r in rows] for i in range(8)]
widths = [max(len(n), max(len(v) for v in vals) if vals else 0) + 2 for n, vals in zip(col_names, col_vals)]
hdr = ''.join(f"{n:<{w}}" for n, w in zip(col_names, widths))
output += hdr + "\n"
output += "─" * len(hdr) + "\n"
for r in rows:
output += ''.join(f"{r[i]:<{widths[i]}}" for i in range(8)) + "\n"
else:
output += " (No data)\n"
# ── LVM Combined Volumes ──
output += "\n🔗 LVM Combined Volumes\n"
if 'LVM Combined Volumes' in sections:
lines = [l for l in sections['LVM Combined Volumes'] if l.strip()]
if any('VG Name' in l for l in lines):
# parseVG/LV/PV
vgs, lvs, pvs = [], [], []
section = ''
for l in lines:
if l.strip().startswith('---'):
section = l.strip('-').strip().lower()
continue
if section == 'volume group':
if 'VG Name' in l:
vgs.append({'name': l.split('VG Name')[-1].strip()})
elif 'VG Size' in l and vgs:
vgs[-1]['size'] = l.split('VG Size')[-1].strip()
elif section == 'logical volume':
if 'LV Name' in l:
lvs.append({'name': l.split('LV Name')[-1].strip()})
elif 'VG Name' in l and lvs:
lvs[-1]['vg'] = l.split('VG Name')[-1].strip()
elif 'LV Size' in l and lvs:
lvs[-1]['size'] = l.split('LV Size')[-1].strip()
elif section == 'physical volume':
if 'PV Name' in l:
pvs.append({'name': l.split('PV Name')[-1].strip()})
elif 'VG Name' in l and pvs:
pvs[-1]['vg'] = l.split('VG Name')[-1].strip()
elif 'PV Size' in l and pvs:
pvs[-1]['size'] = l.split('PV Size')[-1].strip().split('/')[0].strip()
for vg in vgs:
vg_name = vg.get('name', '?')
vg_size = vg.get('size', '?')
# Find PVs and LVs belonging to this VG
vg_pvs = [p for p in pvs if p.get('vg') == vg_name]
vg_lvs = [l for l in lvs if l.get('vg') == vg_name]
pv_count = len(vg_pvs)
pv_names = [p.get('name', '?') for p in vg_pvs]
pv_sizes = [p.get('size', '?') for p in vg_pvs]
lv_info = ', '.join(f"{l.get('name','?')} ({l.get('size','?')})" for l in vg_lvs)
output += f"\n{pv_count}disks merged into {vg_name},mounted at /home\n"
# ASCII art - PV sorted by name
sorted_pvs = sorted(pv_names)
if pv_count > 0:
for i, pn in enumerate(sorted_pvs):
if i == 0:
output += f"{pn} ─┐\n"
elif i < pv_count - 1:
output += f"{pn} ─┤\n"
else:
output += f"{pn} ─┘── {vg_name} ── {lv_info} ── /home\n"
else:
output += " (No LVM configuration)\n"
else:
output += " (No LVM configuration)\n"
# ── Partition Usage (table) ──
output += "\n📊 Partition Usage status\n"
if 'Partition Usage' in sections:
lines = [l for l in sections['Partition Usage'] if l.strip()]
if lines:
# Parse df output
partitions = []
for l in lines:
parts = l.split()
if len(parts) >= 6 and parts[0] != 'Filesystem':
fs = parts[0]
# Simplify filesystem names
if '/dev/mapper/' in fs:
fs_short = fs.replace('/dev/mapper/', '')
elif '/dev/' in fs:
fs_short = fs.replace('/dev/', '')
else:
fs_short = fs
mount = parts[5] if len(parts) > 5 else parts[-1]
label = f"{fs_short} → {mount}"
partitions.append((label, parts[1], parts[2], parts[3], parts[4]))
if partitions:
label_w = max(len(p[0]) for p in partitions) + 2
label_w = max(label_w, 12)
size_w = max(len(p[1]) for p in partitions) + 2
size_w = max(size_w, 8)
used_w = max(len(p[2]) for p in partitions) + 2
used_w = max(used_w, 8)
avail_w = max(len(p[3]) for p in partitions) + 2
avail_w = max(avail_w, 8)
pct_w = max(len(p[4]) for p in partitions) + 2
pct_w = max(pct_w, 8)
hdr = f"{'Partition':<{label_w}}{'Total':<{size_w}}{'Used':<{used_w}}{'Available':<{avail_w}}{'Usage%':<{pct_w}}"
output += hdr + "\n"
output += "─" * len(hdr) + "\n"
for p in partitions:
output += f"{p[0]:<{label_w}}{p[1]:<{size_w}}{p[2]:<{used_w}}{p[3]:<{avail_w}}{p[4]:<{pct_w}}\n"
else:
output += " (No data)\n"
else:
output += " (No data)\n"
# ── Docker Directory Mapping ──
output += "\n🐳 Docker Directory Mapping\n"
if 'Docker Directory Mapping' in sections:
lines = [l for l in sections['Docker Directory Mapping'] if l.strip()]
if lines:
in_df = False
for l in lines:
if 'Docker Disk Usage' in l or l.startswith('TYPE'):
in_df = True
if in_df:
output += f" {l}\n"
continue
if l.startswith('--') and l.endswith('--'):
cname = l.strip('-').strip()
output += f"\n 📦 {cname}\n"
elif 'bind:' in l or 'volume:' in l:
# Format mapping line
l = l.strip()
arrow_pos = l.find('->')
if arrow_pos > 0:
src_dst = l[arrow_pos+2:].strip()
src_start = l.find(':') + 2
src = l[src_start:arrow_pos].strip()
output += f" {src} → {src_dst}\n"
else:
output += f" {l}\n"
else:
output += " (No running containers)\n"
return output
def _detect_disks(self) -> str:
if not self.session_manager.active_session:
return 'Please establish SSH connection first'
result = self.session_manager.execute_command('lsblk -o NAME,SIZE,TYPE,MOUNTPOINT 2>/dev/null || lsblk', timeout=30)
if result.error:
return f"❌ Execution failed [{result.error}]: {result.message}"
output = result.stdout or result.stderr
unmounted_disks, mounted_disks = [], []
for line in output.split('\n'):
line = line.strip()
if not line or line.startswith('NAME'):
continue
parts = re.split(r'\s+', line)
if len(parts) >= 3:
name, size = parts[0], parts[1]
mountpoint = parts[-1] if len(parts) > 3 else 'none'
is_physical = (
(name.startswith('nvme') and 'n1' in name and 'p' not in name) or
re.match(r'^[sh]d[a-z]$', name) or
re.match(r'^x?vd[a-z]$', name) or
re.match(r'^mmcblk\d+$', name)
)
if is_physical:
is_mounted = mountpoint and mountpoint != 'none' and '/run/media' not in mountpoint
if is_mounted:
mounted_disks.append({'name': name, 'size': size, 'mountpoint': mountpoint})
else:
unmounted_disks.append({'name': name, 'size': size})
response = '📊 Disk detection result\n\n'
if unmounted_disks:
response += '⚠️ Unmounted disks:\n'
for disk in unmounted_disks:
response += f" - /dev/{disk['name']},size {disk['size']}\n"
if len(unmounted_disks) >= 2:
example_disks = ', '.join([d['name'] for d in unmounted_disks[:2]])
response += f'\n💡 Merge example: {example_disks}merged and mounted to data\n'
else:
response += '✅ Nounmounted disk\n'
if mounted_disks:
response += '\n📁 Mounted disks:\n'
for disk in mounted_disks:
response += f" - /dev/{disk['name']},{disk['size']},mounted at {disk['mountpoint']}\n"
return response
def _confirm_disk_merge(self, merge_info: Dict[str, Any]) -> str:
disks, mount_point = merge_info['disks'], merge_info['mount_point']
self.pending_disk_merge = merge_info
return (
f"⚠️⚠️⚠️ Dangerous operation confirm ⚠️⚠️⚠️\n\n"
f"📌 Merge disks: {', '.join(disks)}\n"
f"📌 Mount point: {mount_point}\n\n"
f"⚠️ This will format all specified disks, all data will be erased!\n\n"
f'Please reply "confirm" to continue, or "cancel" to abort.'
)
def _execute_disk_merge(self, merge_info: Dict[str, Any]) -> str:
disks, mount_point = merge_info['disks'], merge_info['mount_point']
if not self.session_manager.active_session:
self.pending_disk_merge = None
return 'Please establish SSH connection first'
disk_paths = [f'/dev/{disk}' for disk in disks]
vg_name, lv_name = 'vgdata', 'lvdata'
merge_script = f"""
set -e
which pvcreate || (apt-get update && apt-get install -y lvm2) > /dev/null 2>&1 || true
for disk in {' '.join(disk_paths)}; do pvcreate -y "$disk"; done
vgcreate {vg_name} {' '.join(disk_paths)}
lvcreate -l 100%FREE -n {lv_name} {vg_name}
mkfs.ext4 /dev/{vg_name}/{lv_name}
mkdir -p {mount_point}
mount /dev/{vg_name}/{lv_name} {mount_point}
echo "/dev/{vg_name}/{lv_name} {mount_point} ext4 defaults 0 2" >> /etc/fstab
echo "=== Merge completed ==="
df -h {mount_point}
"""
self.pending_disk_merge = None
result = self.session_manager.execute_command(merge_script, timeout=300)
if result.error:
return f"❌ Disk merge failed: {result.message}\n{result.stderr}"
output = f"✅ Disk merge succeeded!\n📌 Disks: {', '.join(disks)}\n📌 Mount point: {mount_point}\n\n"
output += result.stdout or ''
return output
def _handle_auto_mount(self, text: str) -> str:
if not self.session_manager.active_session:
return 'Please establish SSH connection first'
if 'check' in text and 'fstab' in text:
return self._exec_simple('fstab configuration', 'cat /etc/fstab')
if 'fix' in text and 'fstab' in text:
self.pending_confirmation = '_repair_fstab_action'
return '⚠️ Will fix fstab configuration\n\nPlease reply "confirm" or "cancel"'
device, mount_point = None, None
match = re.search(r'(?:set\s*)?([/\w]+)\s*auto mount on boot to\s*([/\w]+)', text)
if match:
device, mount_point = match.group(1), match.group(2)
if not device:
return '❌ Format: Set /dev/sda1 auto mount on boot to /data\nOr: check fstab'
if not device.startswith('/dev/'):
device = '/dev/' + device
if mount_point and not mount_point.startswith('/'):
mount_point = '/' + mount_point
self.pending_confirmation = f'_set_auto_mount {device} {mount_point}'
return f'⚠️ Will set {device} auto mount on boot to {mount_point}\n\nPlease reply "confirm" or "cancel"'
# ========== connection/execution ==========
def _connect(self, info: Dict[str, Any]) -> str:
try:
host, port = info['host'], int(info.get('port', 22))
user, password = info.get('user', 'root'), info.get('password')
success, message = self.session_manager.create_session(host=host, port=port, username=user, password=password, timeout=30)
if success:
return f"✅ SSH connection successful\nhost: {host}:{port}\nuser: {user}\n\nYou can now execute commands\n\n💡 Available natural language commands:\n- CPU info / memory info / system overview\n- container list / image list / container logs xxx\n- port scan / firewall status / network interface\n- disk space / disk health / LVM information\n- login audit / security check\n- system logs / SSH logs\n- Or enter Linux commands directly"
return f"❌ Connection failed: {message}"
except Exception as e:
return f"❌ Connection error: {str(e)}"
def _validate_and_execute(self, command: str) -> str:
# Special internal commands
if command.startswith('_set_auto_mount '):
parts = command.split(' ')
if len(parts) >= 3:
return self._execute_set_auto_mount(parts[1], parts[2])
return '❌ Parameter error'
if command == '_repair_fstab_action':
return self._execute_repair_fstab()
if self.command_validator.is_blocked(command):
return f"⚠️ Command blocked: High risk, execution prohibited"
if self.command_validator.is_sensitive(command):
self.pending_confirmation = command
return f'⚠️ Security warning\n\nThis involves sensitive operation, confirm to execute?\n\nCommand: {command}\n\nPlease reply "confirm" or "cancel"'
return self._execute_command(command)
def _execute_command(self, command: str) -> str:
result = self.session_manager.execute_command(command, timeout=60)
if result.error:
return f"❌ Execution failed [{result.error}]: {result.message}"
output = f"📝 Execution result\nCommand: {result.command}\nhost: {result.target_host}\ntime: {result.duration:.2f}s\nExit code: {result.exit_code}\n\n"
if result.stdout:
output += f"STDOUT:\n{result.stdout}\n"
if result.stderr:
output += f"STDERR:\n{result.stderr}\n"
return output
def _execute_set_auto_mount(self, device: str, mount_point: str) -> str:
if not self.session_manager.active_session:
return 'Please establish SSH connection first'
self.session_manager.execute_command(f'mkdir -p {mount_point}', timeout=30)
uuid_result = self.session_manager.execute_command(f'blkid {device} | grep -oP "UUID=\\"[^\\"]+\\"" | cut -d\'"\' -f2 2>/dev/null || echo ""', timeout=30)
uuid = (uuid_result.stdout or '').strip()
fs_result = self.session_manager.execute_command(f'blkid {device} | grep -oP "TYPE=\\"[^\\"]+\\"" | cut -d\'"\' -f2 2>/dev/null || echo "ext4"', timeout=30)
fs_type = (fs_result.stdout or 'ext4').strip()
fstab_entry = f"UUID={uuid} {mount_point} {fs_type} defaults 0 2" if uuid else f"{device} {mount_point} {fs_type} defaults 0 2"
self.session_manager.execute_command(f'cp /etc/fstab /etc/fstab.bak.$(date +%Y%m%d%H%M%S)', timeout=30)
self.session_manager.execute_command(f'echo "{fstab_entry}" >> /etc/fstab', timeout=30)
test_result = self.session_manager.execute_command('mount -a 2>&1', timeout=30)
if test_result.stderr and 'failed' in test_result.stderr.lower():
self.session_manager.execute_command('cp /etc/fstab.bak* /etc/fstab 2>/dev/null || true', timeout=30)
return f"❌ Mount test failed, rolled back\n{test_result.stderr}"
output = f"✅ Auto mount on boot configured successfully!\n📌 Device: {device}\n📌 Mount point: {mount_point}\n📌 entry: {fstab_entry}\n"
verify = self.session_manager.execute_command(f'df -h {mount_point}', timeout=30)
if verify.stdout:
output += f"\n{verify.stdout}"
return output
def _execute_repair_fstab(self) -> str:
if not self.session_manager.active_session:
return 'Please establish SSH connection first'
self.session_manager.execute_command('cp /etc/fstab /etc/fstab.repair.bak.$(date +%Y%m%d%H%M%S)', timeout=30)
check = self.session_manager.execute_command('mount -a 2>&1', timeout=30)
if check.stderr:
return f"⚠️ fstab has issues:\n{check.stderr}\n\nSuggest manual check /etc/fstab"
return "✅ fstab configuration is valid, mount test passed"
def _list_connections(self) -> str:
sessions = self.session_manager.get_session_info()
if not sessions:
return 'No active SSH connection'
output = '📡 Active connections list\n\n'
for idx, session in enumerate(sessions, 1):
active_mark = ' ⭐' if session['is_active'] else ''
output += f"{idx}. {session['host']}:{session['port']} (user: {session['username']}){active_mark}\n"
return output
def _disconnect(self) -> str:
if self.session_manager.active_session:
host = self.session_manager.active_session.conn_info.host
self.session_manager.close_session()
return f"Disconnected from {host} SSH connection"
return 'No active SSH connection'
def to_json_response(self, text: str) -> str:
result = self.handle_command(text)
return json.dumps({'type': 'text', 'content': result}, ensure_ascii=False, indent=2)#!/usr/bin/env python3
import sys
import os
import argparse
import subprocess
import time
import traceback
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from scripts.executor import CommandExecutor
def _get_ctrl_path(host, port, user):
"""Get SSH ControlMaster socket path"""
return f"/tmp/ssh-ctrl-{user}@{host}:{port}"
def _is_ctrl_alive(ctrl_path):
"""Check if ControlMaster socket is alive"""
if not os.path.exists(ctrl_path):
return False
check = subprocess.run(
['ssh', '-o', f'ControlPath={ctrl_path}', '-o', 'ControlMaster=auto',
'-o', 'BatchMode=yes', '-o', 'ConnectTimeout=2', 'localhost', 'echo', 'ok'],
capture_output=True, text=True, timeout=3
)
return check.returncode == 0
def _ensure_connection(host, port, user, password):
"""Ensure SSH ControlMaster connection is established, return ctrl_path"""
ctrl_path = _get_ctrl_path(host, port, user)
# Check if existing connection is available
if _is_ctrl_alive(ctrl_path):
return ctrl_path
# Clean up stale socket
if os.path.exists(ctrl_path):
os.unlink(ctrl_path)
# Establish new connection (first time is slower, ~1-2 seconds)
ssh_opts = [
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', f'ControlPath={ctrl_path}',
'-o', 'ControlMaster=auto',
'-o', 'ControlPersist=10m', # Auto close after 10 minutes idle
'-o', 'ServerAliveInterval=30',
'-o', 'ServerAliveCountMax=3',
'-p', str(port),
]
# Prefer sshpass (password auth) if available, otherwise try key auth
if password and os.path.exists('/usr/bin/sshpass'):
cmd = ['sshpass', '-p', password, 'ssh'] + ssh_opts + [f'{user}@{host}', 'true']
else:
cmd = ['ssh'] + ssh_opts + [f'{user}@{host}', 'true']
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if result.returncode != 0:
return None
return ctrl_path
def _is_shell_command(command):
"""Determine if command is a direct shell command (not natural language)
Shell command characteristics: starts with known commands or contains shell syntax"""
# Common shell command prefixes (sorted by length descending for longest match first)
shell_prefixes = [
# Network tools (full commands, not substrings)
'curl', 'wget', 'nc', 'scp', 'rsync',
# System commands
'ps', 'top', 'htop', 'kill', 'pkill', 'killall',
'ls', 'll', 'cat', 'head', 'tail', 'grep', 'awk', 'sed', 'find', 'wc',
'df', 'du', 'free', 'uptime', 'who', 'w', 'id', 'uname',
'mkdir', 'rmdir', 'cp', 'mv', 'rm', 'touch', 'chmod', 'chown',
'tar', 'gzip', 'gunzip', 'zip', 'unzip',
'echo', 'printf', 'date', 'cal', 'sleep',
'nohup', 'screen', 'tmux',
'docker', 'docker-compose', 'kubectl',
'python3', 'python', 'pip', 'pip3', 'node', 'npm',
'bash', 'sh', 'source', 'export', 'env', 'which', 'whereis',
'systemctl', 'service', 'journalctl', 'dmesg',
'ss ', 'netstat ', 'ip ', 'ifconfig', 'ping ', 'traceroute', 'nslookup', 'dig',
'iptables', 'ufw', 'firewall-cmd',
'nvidia-smi', 'npu-smi',
'git', 'vim', 'vi', 'nano',
# Assignment and pipes
'export ', 'VAR=',
]
cmd_stripped = command.strip()
for prefix in shell_prefixes:
if cmd_stripped.startswith(prefix):
return True
# Contains shell syntax patterns
shell_patterns = ['|', '&&', '||', '>', '>>', ';', '$(', '`', '2>&1', '-c ']
if any(p in command for p in shell_patterns):
return True
return False
def run_one_shot_fast(host, port, user, password, command, raw=False):
"""Fast mode: Use SSH ControlMaster to reuse connection
First time ~1.5s, subsequent ~0.2s
When raw=True, skip natural language parsing and execute raw command directly"""
ctrl_path = _ensure_connection(host, port, user, password)
if not ctrl_path:
return run_one_shot_paramiko(host, port, user, password, command, raw=raw)
# Raw mode: execute directly, bypass NL routing
if raw:
result = subprocess.run(
['ssh', '-o', f'ControlPath={ctrl_path}', '-o', 'ControlMaster=auto',
'-o', 'BatchMode=yes', '-p', str(port), f'{user}@{host}', command],
capture_output=True, text=True, timeout=60
)
if result.returncode == 0:
if result.stdout.strip():
print(result.stdout.strip())
else:
if result.stderr.strip():
print(f"❌ Error (exit {result.returncode}): {result.stderr.strip()}")
elif result.stdout.strip():
print(result.stdout.strip())
return result.returncode
# Determine if command is natural language
# Strategy: if it looks like a shell command, execute directly; otherwise route to NL
is_nl = not _is_shell_command(command)
if is_nl:
# Natural language goes through executor (requires paramiko connection)
executor = CommandExecutor()
connect_text = f"SSH connect {host} port {port} user {user} password {password}"
executor.handle_command(connect_text)
result = executor.handle_command(command)
print(result)
executor.session_manager.close_all_sessions()
return 0
else:
# Direct shell command goes through fast mode
result = subprocess.run(
['ssh', '-o', f'ControlPath={ctrl_path}', '-o', 'ControlMaster=auto',
'-o', 'BatchMode=yes', '-p', str(port), f'{user}@{host}', command],
capture_output=True, text=True, timeout=60
)
if result.returncode == 0:
if result.stdout.strip():
print(result.stdout.strip())
else:
if result.stderr.strip():
print(f"❌ Error (exit {result.returncode}): {result.stderr.strip()}")
elif result.stdout.strip():
print(result.stdout.strip())
return result.returncode
def run_one_shot_paramiko(host, port, user, password, command, raw=False):
"""Paramiko mode: connect -> execute -> disconnect (fallback)
When raw=True, execute command directly without NL routing"""
executor = CommandExecutor()
connect_text = f"SSH connect {host} port {port} user {user} password {password}"
result = executor.handle_command(connect_text)
if 'failed' in result.lower() or 'error' in result.lower():
print(result)
return 1
if raw:
# Raw mode: execute directly via paramiko, bypass NL routing
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(host, port=port, username=user, password=password)
stdin, stdout, stderr = ssh.exec_command(command, timeout=60)
out = stdout.read().decode()
err = stderr.read().decode()
ssh.close()
if out.strip():
print(out.strip())
if err.strip():
print(f"⚠️ stderr: {err.strip()}")
return 0 if not err.strip() else 1
result = executor.handle_command(command)
print(result)
executor.session_manager.close_all_sessions()
return 0
def run_one_shot(host, port, user, password, command, raw=False):
"""One-shot mode: prefer fast mode, fallback to paramiko"""
return run_one_shot_fast(host, port, user, password, command, raw=raw)
def main():
# Parse --command one-time execution arguments
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--host', help='SSH host IP')
parser.add_argument('--port', type=int, default=22, help='SSH port (default 22)')
parser.add_argument('--user', default='root', help='SSH username (default root)')
parser.add_argument('--password', help='SSH password')
parser.add_argument('--command', help='command to execute (one-time mode)')
parser.add_argument('--raw', action='store_true', help='Skip NL parsing, execute raw command')
# Parse known args first, ignore unknown (leave for NL mode)
args, remaining = parser.parse_known_args()
# One-time execution mode
if args.host and args.password and args.command:
sys.exit(run_one_shot(args.host, args.port, args.user, args.password, args.command, raw=args.raw))
executor = CommandExecutor()
# NL mode: execute with provided params
if remaining or (args.host and not args.command):
text_parts = []
if args.host:
text_parts.append(f"SSH connect {args.host} port {args.port} user {args.user} password {args.password or ''}")
text_parts.extend(remaining)
text = ' '.join(text_parts)
if text.strip():
result = executor.handle_command(text)
print(result)
return
if len(sys.argv) > 1 and not remaining and not args.host:
# No named args, treat as NL command
text = ' '.join(sys.argv[1:])
result = executor.handle_command(text)
print(result)
return
print("SSH Remote Executor - Interactive Mode")
print("======================================")
print("Type 'exit' or 'quit' to exit")
print("Type 'help' for help")
print("======================================\n")
# Dead loop protection - only idle timeout (suitable for remote connection scenarios)
idle_timeout = 600 # Auto exit after 10 minutes idle
last_activity_time = time.time()
while True:
try:
# Check idle timeout (only after user has activity)
current_time = time.time()
if last_activity_time > 0 and (current_time - last_activity_time > idle_timeout):
executor.session_manager.close_all_sessions()
print(f"\nIdle timeout ({idle_timeout} seconds), auto exit")
break
# Get user input
text = input("> ")
text = text.strip()
# Update activity time
last_activity_time = time.time()
if text.lower() in ('exit', 'quit', 'bye'):
executor.session_manager.close_all_sessions()
print("Exited, all connections closed")
break
if text.lower() == 'help':
print("""
Help Information:
Connect command:
SSH connect 192.168.1.100 port 22 user root password xxx
Execute commands:
Enter Linux commands directly, e.g.: ls -la, df -h, top -bn1
System commands:
view connections - List all active connections
disconnect/disconnect SSH - Disconnect current connection
cancel - Cancel pending confirmation
confirm - Confirm sensitive operation
exit/quit - Exit program
Security Features:
- Sensitive operations (delete, modify, etc.) require confirmation
- High-risk commands are automatically blocked
- Passwords are only stored in memory, not written to disk
""")
continue
result = executor.handle_command(text)
print(result)
print()
except KeyboardInterrupt:
executor.session_manager.close_all_sessions()
print("\nExited, all connections closed")
break
except EOFError:
executor.session_manager.close_all_sessions()
print("\nInput ended, exiting")
break
except Exception as e:
print(f"Error: {str(e)}")
traceback.print_exc()
if __name__ == '__main__':
main()
import time
from typing import Dict, Optional, List, Tuple
from .ssh_client import SSHClient, ConnectionInfo, SSHResult, get_pool
class SessionManager:
_instance = None
_lock = False
def __new__(cls):
if cls._instance is None:
cls._instance = super(SessionManager, cls).__new__(cls)
cls._instance._sessions = {}
cls._instance._active_session = None
cls._instance._max_sessions = 10
cls._instance._session_timeout = 3600 # 1 hour
cls._instance._pool = None # lazy initialization
return cls._instance
@property
def pool(self):
"""Get connection pool instance"""
if self._pool is None:
self._pool = get_pool()
return self._pool
@property
def sessions(self) -> Dict[str, SSHClient]:
self._cleanup_expired()
return self._sessions
@property
def active_session(self) -> Optional[SSHClient]:
if self._active_session and self._active_session in self._sessions:
return self._sessions[self._active_session]
return None
def create_session(self, host: str, port: int, username: str,
password: Optional[str] = None, timeout: int = 30) -> Tuple[bool, str]:
self._cleanup_expired()
if len(self._sessions) >= self._max_sessions:
return False, f"Maximum connection limit reached ({self._max_sessions})"
# Check for existing connection (using pool reuse)
for client in self._sessions.values():
if client.conn_info.host == host and client.conn_info.port == port:
self._active_session = client.session_id
return True, f"Already connected to {host}:{port} (reusing connection)"
conn_info = ConnectionInfo(
host=host,
port=port,
username=username,
password=password,
timeout=timeout
)
try:
# Get connection from pool (auto-reuse)
client = self.pool.get_connection(conn_info)
self._sessions[client.session_id] = client
self._active_session = client.session_id
fingerprint = client.get_host_fingerprint()
return True, f"Successfully connected to {host}:{port}\nHost fingerprint: {fingerprint}\n (Connection pool enabled, auto-disconnect after 10 minutes idle)"
except ValueError as e:
return False, str(e)
def get_session(self, session_id: str) -> Optional[SSHClient]:
return self._sessions.get(session_id)
def set_active_session(self, session_id: str) -> bool:
if session_id in self._sessions:
self._active_session = session_id
return True
return False
def switch_by_host(self, host: str) -> bool:
for session_id, client in self._sessions.items():
if client.conn_info.host == host:
self._active_session = session_id
return True
return False
def close_session(self, session_id: Optional[str] = None) -> bool:
if session_id is None:
session_id = self._active_session
if session_id and session_id in self._sessions:
self._sessions[session_id].close()
del self._sessions[session_id]
if self._active_session == session_id:
if self._sessions:
self._active_session = next(iter(self._sessions.keys()))
else:
self._active_session = None
return True
return False
def close_all_sessions(self) -> None:
for client in self._sessions.values():
client.close()
self._sessions.clear()
self._active_session = None
def execute_command(self, command: str, timeout: int = 60,
session_id: Optional[str] = None) -> SSHResult:
target_session = session_id or self._active_session
if not target_session or target_session not in self._sessions:
return SSHResult(
session_id='',
target_host='',
command=command,
exit_code=None,
stdout='',
stderr='',
duration=0,
timestamp=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
error='no_session',
message='No available SSH connection'
)
client = self._sessions[target_session]
return client.execute(command, timeout)
def get_session_info(self) -> List[Dict[str, str]]:
info = []
for session_id, client in self._sessions.items():
info.append({
'session_id': session_id,
'host': client.conn_info.host,
'port': client.conn_info.port,
'username': client.conn_info.username,
'connected': str(client.connected),
'is_active': session_id == self._active_session
})
return info
def _cleanup_expired(self) -> None:
now = time.time()
expired = []
for session_id, client in self._sessions.items():
if client.connect_time and (now - client.connect_time) > self._session_timeout:
expired.append(session_id)
for session_id in expired:
self.close_session(session_id)
def get_active_host(self) -> Optional[str]:
if self.active_session:
return self.active_session.conn_info.host
return None
def session_count(self) -> int:
return len(self._sessions)
def get_pool_status(self) -> List[Dict[str, str]]:
"""Get connection pool status"""
return self.pool.get_pool_status()
def set_idle_timeout(self, seconds: int):
"""Set connection pool idle timeout"""
self.pool.set_idle_timeout(seconds)
import base64
import hashlib
import select
import time
import uuid
import threading
from dataclasses import dataclass
from typing import Optional, Tuple, Dict, Any, List
from contextlib import contextmanager
import paramiko
from paramiko.ssh_exception import SSHException, AuthenticationException, NoValidConnectionsError
@dataclass
class SSHResult:
session_id: str
target_host: str
command: str
exit_code: Optional[int]
stdout: str
stderr: str
duration: float
timestamp: str
error: Optional[str] = None
message: Optional[str] = None
@dataclass
class ConnectionInfo:
host: str
port: int
username: str
password: Optional[str] = None
key_filename: Optional[str] = None
timeout: int = 30
class SSHClient:
def __init__(self, conn_info: ConnectionInfo):
self.conn_info = conn_info
self.client: Optional[paramiko.SSHClient] = None
self.session_id = str(uuid.uuid4())
self.connected = False
self.connect_time: Optional[float] = None
def connect(self) -> bool:
try:
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
connect_kwargs: Dict[str, Any] = {
'hostname': self.conn_info.host,
'port': self.conn_info.port,
'username': self.conn_info.username,
'timeout': self.conn_info.timeout,
'look_for_keys': False,
'allow_agent': False,
}
if self.conn_info.password:
connect_kwargs['password'] = self.conn_info.password
elif self.conn_info.key_filename:
connect_kwargs['key_filename'] = self.conn_info.key_filename
connect_kwargs['look_for_keys'] = True
else:
raise ValueError("Must provide password or key file")
self.client.connect(**connect_kwargs)
self.connected = True
self.connect_time = time.time()
return True
except AuthenticationException:
self._cleanup()
raise ValueError(f"Authentication failed: invalid username or password")
except NoValidConnectionsError:
self._cleanup()
raise ValueError(f"Cannot connect to {self.conn_info.host}:{self.conn_info.port}")
except SSHException as e:
self._cleanup()
raise ValueError(f"SSH connection error: {str(e)}")
except Exception as e:
self._cleanup()
raise ValueError(f"Connection failed: {str(e)}")
def execute(self, command: str, timeout: int = 60) -> SSHResult:
if not self.connected or not self.client:
return SSHResult(
session_id=self.session_id,
target_host=self.conn_info.host,
command=command,
exit_code=None,
stdout='',
stderr='',
duration=0,
timestamp=self._get_timestamp(),
error='not_connected',
message='SSH connection not established'
)
start_time = time.time()
try:
stdin, stdout, stderr = self.client.exec_command(command, timeout=timeout)
output = ''
errors = ''
exit_code = None
end_time = start_time + timeout
while time.time() < end_time:
if stdout.channel.recv_ready():
output += stdout.read(4096).decode('utf-8', errors='replace')
if stderr.channel.recv_stderr_ready():
errors += stderr.read(4096).decode('utf-8', errors='replace')
if stdout.channel.exit_status_ready():
exit_code = stdout.channel.recv_exit_status()
break
time.sleep(0.1)
if exit_code is None:
stdout.channel.close()
return SSHResult(
session_id=self.session_id,
target_host=self.conn_info.host,
command=command,
exit_code=None,
stdout=output,
stderr=errors,
duration=time.time() - start_time,
timestamp=self._get_timestamp(),
error='timeout',
message='Command execution timeout'
)
return SSHResult(
session_id=self.session_id,
target_host=self.conn_info.host,
command=command,
exit_code=exit_code,
stdout=output,
stderr=errors,
duration=time.time() - start_time,
timestamp=self._get_timestamp()
)
except SSHException as e:
return SSHResult(
session_id=self.session_id,
target_host=self.conn_info.host,
command=command,
exit_code=None,
stdout='',
stderr='',
duration=time.time() - start_time,
timestamp=self._get_timestamp(),
error='ssh_error',
message=f"SSH execution error: {str(e)}"
)
except Exception as e:
return SSHResult(
session_id=self.session_id,
target_host=self.conn_info.host,
command=command,
exit_code=None,
stdout='',
stderr='',
duration=time.time() - start_time,
timestamp=self._get_timestamp(),
error='error',
message=f"Execution failed: {str(e)}"
)
def close(self) -> None:
self._cleanup()
def _cleanup(self) -> None:
if self.client:
try:
self.client.close()
except:
pass
self.client = None
self.connected = False
@staticmethod
def _get_timestamp() -> str:
return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
def get_host_fingerprint(self) -> Optional[str]:
if not self.connected or not self.client:
return None
try:
transport = self.client.get_transport()
if transport:
remote_key = transport.get_remote_server_key()
digest = hashlib.sha256(remote_key.asbytes()).digest()
return 'SHA256:' + base64.b64encode(digest).decode('ascii').rstrip('=')
except:
pass
return None
class ConnectionPool:
"""SSH connection pool with connection reuse, auto-disconnect, and rate limiting"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self._initialized = True
self._pool: Dict[str, SSHClient] = {} # key: "host:port"
self._last_used: Dict[str, float] = {} # last used time
self._pool_lock = threading.Lock()
# Protection mechanism configuration
self._max_connections = 50 # max connections (multi-target support)
self._max_concurrent_requests = 200 # max concurrent requests
self._idle_timeout = 600 # auto-disconnect after 10 minutes idle
self._connect_timeout = 10 # connection timeout (seconds)
self._execute_timeout = 60 # execution timeout (seconds)
self._health_check_interval = 30 # health check interval (seconds)
# request rate limiting semaphore
self._request_semaphore = threading.Semaphore(self._max_concurrent_requests)
# statistics
self._total_requests = 0
self._failed_requests = 0
self._timeout_requests = 0
# background threads
self._cleanup_thread: Optional[threading.Thread] = None
self._health_thread: Optional[threading.Thread] = None
self._running = False
self._start_background_threads()
def _get_key(self, host: str, port: int) -> str:
return f"{host}:{port}"
def _start_background_threads(self):
"""Start background threads"""
self._running = True
# cleanup idle connections thread
self._cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
self._cleanup_thread.start()
# health check thread
self._health_thread = threading.Thread(target=self._health_check_loop, daemon=True)
self._health_thread.start()
def _cleanup_loop(self):
"""Periodically clean up idle connections"""
while self._running:
time.sleep(60) # check every minute
self._cleanup_idle_connections()
def _health_check_loop(self):
"""Periodic health check"""
while self._running:
time.sleep(self._health_check_interval)
self._check_connection_health()
def _check_connection_health(self):
"""Check connection health status, remove bad connections"""
with self._pool_lock:
keys_to_remove = []
for key, client in self._pool.items():
try:
# Check if connection is alive
if not client.connected or not client.client:
keys_to_remove.append(key)
continue
transport = client.client.get_transport()
if not transport or not transport.is_active():
keys_to_remove.append(key)
continue
# Send heartbeat to detect connection
transport.send_ignore()
except Exception:
keys_to_remove.append(key)
for key in keys_to_remove:
client = self._pool.pop(key, None)
self._last_used.pop(key, None)
if client:
try:
client.close()
except:
pass
def _cleanup_idle_connections(self):
"""Clean up timed-out idle connections"""
now = time.time()
with self._pool_lock:
keys_to_remove = []
for key, last_used in self._last_used.items():
if now - last_used > self._idle_timeout:
keys_to_remove.append(key)
for key in keys_to_remove:
client = self._pool.pop(key, None)
self._last_used.pop(key, None)
if client:
try:
client.close()
except:
pass
def get_connection(self, conn_info: ConnectionInfo, timeout: Optional[int] = None) -> SSHClient:
"""Get connection (reuse or create), with rate limiting
Args:
conn_info: connection info
timeout: connection timeout (seconds), None for default
Returns:
SSHClient instance
Raises:
ValueError: Connection failed or exceeded max connections
"""
key = self._get_key(conn_info.host, conn_info.port)
connect_timeout = timeout or self._connect_timeout
with self._pool_lock:
# Check for available connections
if key in self._pool:
client = self._pool[key]
if client.connected and client.client and client.client.get_transport() and client.client.get_transport().is_active():
self._last_used[key] = time.time()
return client
else:
# Connection disconnected, remove
self._pool.pop(key, None)
self._last_used.pop(key, None)
# Check max connections limit
if len(self._pool) >= self._max_connections:
raise ValueError(f"Maximum connection limit reached ({self._max_connections}), please try again later")
# Create new connection (with timeout protection)
client = SSHClient(conn_info)
client.conn_info.timeout = connect_timeout # Set connection timeout
try:
client.connect()
except Exception as e:
self._failed_requests += 1
raise
self._pool[key] = client
self._last_used[key] = time.time()
return client
def execute(self, conn_info: ConnectionInfo, command: str, timeout: Optional[int] = None) -> SSHResult:
"""Execute command (auto-reuse connection), with rate limiting
Args:
conn_info: connection info
command: command to execute
timeout: execution timeout (seconds), None for default
Returns:
SSHResult execution result
"""
execute_timeout = timeout or self._execute_timeout
self._total_requests += 1
# Use semaphore to limit concurrent requests
acquired = self._request_semaphore.acquire(timeout=30) # max wait 30 seconds for semaphore
if not acquired:
self._timeout_requests += 1
return SSHResult(
session_id='',
target_host=conn_info.host,
command=command,
exit_code=None,
stdout='',
stderr='',
duration=0,
timestamp=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
error='rate_limit',
message=f'Too many requests, please try again later (current limit: {self._max_concurrent_requests})'
)
try:
client = self.get_connection(conn_info)
result = client.execute(command, execute_timeout)
# Update last used time
key = self._get_key(conn_info.host, conn_info.port)
with self._pool_lock:
self._last_used[key] = time.time()
if result.error == 'timeout':
self._timeout_requests += 1
return result
except ValueError as e:
self._failed_requests += 1
return SSHResult(
session_id='',
target_host=conn_info.host,
command=command,
exit_code=None,
stdout='',
stderr='',
duration=0,
timestamp=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
error='connection_failed',
message=str(e)
)
finally:
self._request_semaphore.release()
def close_connection(self, host: str, port: int) -> bool:
"""Close specified connection"""
key = self._get_key(host, port)
with self._pool_lock:
client = self._pool.pop(key, None)
self._last_used.pop(key, None)
if client:
try:
client.close()
return True
except:
pass
return False
def close_all(self):
"""Close all connections"""
with self._pool_lock:
for client in self._pool.values():
try:
client.close()
except:
pass
self._pool.clear()
self._last_used.clear()
def get_pool_status(self) -> Dict[str, Any]:
"""Get connection pool status"""
with self._pool_lock:
connections = []
now = time.time()
for key, client in self._pool.items():
idle_time = now - self._last_used.get(key, now)
connections.append({
'key': key,
'connected': client.connected,
'idle_seconds': int(idle_time),
'session_id': client.session_id,
'connect_time': client.connect_time
})
return {
'connections': connections,
'total_connections': len(self._pool),
'max_connections': self._max_connections,
'max_concurrent_requests': self._max_concurrent_requests,
'idle_timeout': self._idle_timeout,
'statistics': {
'total_requests': self._total_requests,
'failed_requests': self._failed_requests,
'timeout_requests': self._timeout_requests,
'success_rate': (self._total_requests - self._failed_requests - self._timeout_requests) / max(1, self._total_requests) * 100
}
}
def configure(self,
max_connections: Optional[int] = None,
max_concurrent_requests: Optional[int] = None,
idle_timeout: Optional[int] = None,
connect_timeout: Optional[int] = None,
execute_timeout: Optional[int] = None):
"""Configure connection pool parameters
Args:
max_connections: max connections
max_concurrent_requests: max concurrent requests
idle_timeout: idle timeout (seconds)
connect_timeout: connection timeout (seconds)
execute_timeout: execution timeout (seconds)
"""
if max_connections is not None:
self._max_connections = max_connections
if max_concurrent_requests is not None:
self._max_concurrent_requests = max_concurrent_requests
self._request_semaphore = threading.Semaphore(max_concurrent_requests)
if idle_timeout is not None:
self._idle_timeout = idle_timeout
if connect_timeout is not None:
self._connect_timeout = connect_timeout
if execute_timeout is not None:
self._execute_timeout = execute_timeout
def set_idle_timeout(self, seconds: int):
"""Set idle timeout (seconds)"""
self.configure(idle_timeout=seconds)
def set_max_connections(self, max_conn: int):
"""Set max connections"""
self.configure(max_connections=max_conn)
def set_max_concurrent_requests(self, max_req: int):
"""Set max concurrent requests"""
self.configure(max_concurrent_requests=max_req)
def stop(self):
"""Stop connection pool"""
self._running = False
self.close_all()
# Global connection pool instance
_pool_instance: Optional[ConnectionPool] = None
_pool_lock = threading.Lock()
def get_pool() -> ConnectionPool:
"""Get global connection pool instance"""
global _pool_instance
if _pool_instance is None:
with _pool_lock:
if _pool_instance is None:
_pool_instance = ConnectionPool()
return _pool_instance
@contextmanager
def ssh_connection(conn_info: ConnectionInfo):
"""SSH connection context manager (supports connection pool reuse)"""
pool = get_pool()
client = pool.get_connection(conn_info)
try:
yield client
finally:
# Do not close connection, managed by pool
pass