Add Ansible integration for device management and deployment automation
- Added ansible/ directory with playbooks for: * deploy.yml: Update applications on devices from git * commands.yml: Execute arbitrary commands on devices * system_update.yml: OS updates and health checks * inventory.ini: Device and group configuration * README.md: Comprehensive Ansible guide * requirements.txt: Installation instructions - Added ansible_integration.py: Python module wrapping Ansible operations - Added utils_ansible.py: Updated utilities using Ansible instead of HTTP commands Key benefits: - Idempotent operations with error recovery - Comprehensive logging and backup - Multi-device orchestration - Better reliability and control - Replaces unreliable direct HTTP command execution
This commit is contained in:
388
ansible/README.md
Normal file
388
ansible/README.md
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
# Ansible Integration for Server_Monitorizare
|
||||||
|
|
||||||
|
This directory contains Ansible playbooks and configuration for managing Prezenta Work devices remotely.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Ansible integration replaces direct HTTP command execution with a more robust, scalable solution:
|
||||||
|
|
||||||
|
### **Before (HTTP-based)**
|
||||||
|
```
|
||||||
|
Server → HTTP Request → Device (/execute_command endpoint)
|
||||||
|
Problems:
|
||||||
|
- No idempotency
|
||||||
|
- Hard to track what happened
|
||||||
|
- No built-in error recovery
|
||||||
|
- Limited command execution control
|
||||||
|
```
|
||||||
|
|
||||||
|
### **After (Ansible-based)**
|
||||||
|
```
|
||||||
|
Server → Ansible Playbook → Device (SSH)
|
||||||
|
Benefits:
|
||||||
|
- Idempotent operations
|
||||||
|
- Comprehensive logging
|
||||||
|
- Built-in error handling & retry logic
|
||||||
|
- Structured playbooks
|
||||||
|
- Multi-device orchestration
|
||||||
|
- Backup and rollback capability
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- **inventory.ini** - Defines all devices, groups, and variables
|
||||||
|
|
||||||
|
### Playbooks
|
||||||
|
- **deploy.yml** - Deploy application updates (pull from git, restart service)
|
||||||
|
- **commands.yml** - Execute arbitrary commands on devices
|
||||||
|
- **system_update.yml** - System maintenance (OS updates, health checks)
|
||||||
|
|
||||||
|
### Python Integration
|
||||||
|
- **ansible_integration.py** - Python module for Ansible automation
|
||||||
|
- **utils_ansible.py** - Updated utilities using Ansible (replaces old utils.py)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Install Ansible on Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On Server_Monitorizare (Ubuntu/Debian)
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y ansible
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
ansible --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Inventory
|
||||||
|
|
||||||
|
Edit `inventory.ini` and add your devices:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[prezenta_devices]
|
||||||
|
device_1 ansible_host=192.168.1.20
|
||||||
|
device_2 ansible_host=192.168.1.21
|
||||||
|
device_3 ansible_host=192.168.1.22
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Setup SSH Keys (Optional but Recommended)
|
||||||
|
|
||||||
|
For password-less authentication:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate SSH keys if not present
|
||||||
|
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""
|
||||||
|
|
||||||
|
# Copy public key to each device
|
||||||
|
ssh-copy-id -i ~/.ssh/id_rsa.pub pi@192.168.1.20
|
||||||
|
ssh-copy-id -i ~/.ssh/id_rsa.pub pi@192.168.1.21
|
||||||
|
ssh-copy-id -i ~/.ssh/id_rsa.pub pi@192.168.1.22
|
||||||
|
```
|
||||||
|
|
||||||
|
Update inventory.ini to use SSH key auth:
|
||||||
|
```ini
|
||||||
|
[all:vars]
|
||||||
|
ansible_user=pi
|
||||||
|
ansible_ssh_private_key_file=~/.ssh/id_rsa
|
||||||
|
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Connectivity
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test all devices
|
||||||
|
ansible all -i inventory.ini -m ping
|
||||||
|
|
||||||
|
# Test specific group
|
||||||
|
ansible prezenta_devices -i inventory.ini -m ping
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### From Command Line
|
||||||
|
|
||||||
|
#### Deploy Updates
|
||||||
|
```bash
|
||||||
|
# Deploy dev branch to all devices
|
||||||
|
ansible-playbook -i inventory.ini deploy.yml \
|
||||||
|
-e git_branch=dev \
|
||||||
|
-e backup_before_deploy=true \
|
||||||
|
-e restart_service=true
|
||||||
|
|
||||||
|
# Deploy to specific device
|
||||||
|
ansible-playbook -i inventory.ini deploy.yml \
|
||||||
|
--limit device_1 \
|
||||||
|
-e git_branch=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Execute Commands
|
||||||
|
```bash
|
||||||
|
# Execute command on all devices
|
||||||
|
ansible-playbook -i inventory.ini commands.yml \
|
||||||
|
-e command="ps aux | grep prezenta"
|
||||||
|
|
||||||
|
# Execute on specific device
|
||||||
|
ansible-playbook -i inventory.ini commands.yml \
|
||||||
|
--limit device_1 \
|
||||||
|
-e command="systemctl status prezenta"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### System Update
|
||||||
|
```bash
|
||||||
|
# Update Python packages and perform health check
|
||||||
|
ansible-playbook -i inventory.ini system_update.yml \
|
||||||
|
-e update_python_packages=true \
|
||||||
|
-e perform_health_check=true
|
||||||
|
|
||||||
|
# Also update OS packages
|
||||||
|
ansible-playbook -i inventory.ini system_update.yml \
|
||||||
|
-e update_os_packages=true \
|
||||||
|
-e update_python_packages=true \
|
||||||
|
-e perform_health_check=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Python Code
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils_ansible import (
|
||||||
|
deploy_updates_to_device,
|
||||||
|
deploy_updates_to_all_devices,
|
||||||
|
execute_command_on_device,
|
||||||
|
system_update_device,
|
||||||
|
get_device_status,
|
||||||
|
get_execution_logs
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deploy to specific device
|
||||||
|
result = deploy_updates_to_device('device_1', git_branch='dev')
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# Deploy to all devices
|
||||||
|
result = deploy_updates_to_all_devices(git_branch='dev')
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# Execute command
|
||||||
|
result = execute_command_on_device('device_1', 'systemctl status prezenta')
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# System update
|
||||||
|
result = system_update_device('device_1', update_python=True, health_check=True)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# Get device status
|
||||||
|
result = get_device_status('device_1')
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# Get recent logs
|
||||||
|
logs = get_execution_logs(limit=10)
|
||||||
|
print(logs)
|
||||||
|
```
|
||||||
|
|
||||||
|
### In Flask Routes
|
||||||
|
|
||||||
|
Update your Flask routes to use the new Ansible-based utilities:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import request, jsonify
|
||||||
|
from utils_ansible import (
|
||||||
|
deploy_updates_to_device,
|
||||||
|
execute_command_on_device,
|
||||||
|
get_device_status
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route('/api/deploy', methods=['POST'])
|
||||||
|
def deploy_api():
|
||||||
|
data = request.json
|
||||||
|
device = data.get('device')
|
||||||
|
branch = data.get('branch', 'dev')
|
||||||
|
|
||||||
|
result = deploy_updates_to_device(device, git_branch=branch)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
@app.route('/api/command', methods=['POST'])
|
||||||
|
def command_api():
|
||||||
|
data = request.json
|
||||||
|
device = data.get('device')
|
||||||
|
command = data.get('command')
|
||||||
|
|
||||||
|
result = execute_command_on_device(device, command)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
@app.route('/api/device/<device>/status', methods=['GET'])
|
||||||
|
def device_status_api(device):
|
||||||
|
result = get_device_status(device)
|
||||||
|
return jsonify(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Playbook Details
|
||||||
|
|
||||||
|
### deploy.yml
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Backs up current code (optional)
|
||||||
|
2. Fetches latest from git repository
|
||||||
|
3. Checks out specified branch
|
||||||
|
4. Pulls latest changes
|
||||||
|
5. Verifies syntax
|
||||||
|
6. Restarts application service
|
||||||
|
7. Verifies service is running
|
||||||
|
8. Logs deployment
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
ansible-playbook deploy.yml -e git_branch=dev -e backup_before_deploy=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output Locations:**
|
||||||
|
- Backups: `/srv/prezenta_work/backups/`
|
||||||
|
- Logs: `./logs/deploy_YYYYMMDD_HHMMSS.log`
|
||||||
|
|
||||||
|
### commands.yml
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Executes command with specified user
|
||||||
|
2. Captures output and errors
|
||||||
|
3. Logs command in device's command history
|
||||||
|
4. Returns result
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
ansible-playbook commands.yml -e command="sudo systemctl restart prezenta"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output Locations:**
|
||||||
|
- Logs: `./logs/command_DEVICE_YYYYMMDD_HHMMSS.log`
|
||||||
|
- Device log: `/srv/prezenta_work/data/command_history.log`
|
||||||
|
|
||||||
|
### system_update.yml
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Gathers system facts
|
||||||
|
2. Updates OS packages (optional)
|
||||||
|
3. Updates Python packages
|
||||||
|
4. Checks service status
|
||||||
|
5. Performs health checks (disk, memory, CPU temp)
|
||||||
|
6. Logs results
|
||||||
|
7. Reboots if needed (optional)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
ansible-playbook system_update.yml \
|
||||||
|
-e update_os_packages=false \
|
||||||
|
-e update_python_packages=true \
|
||||||
|
-e perform_health_check=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output Locations:**
|
||||||
|
- Logs: `./logs/system_update_YYYYMMDD_HHMMSS.log`
|
||||||
|
- Device log: `/srv/prezenta_work/data/system_update.log`
|
||||||
|
|
||||||
|
## Execution Logs
|
||||||
|
|
||||||
|
Logs are stored in `ansible/logs/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View recent logs
|
||||||
|
ls -lht ansible/logs/ | head -10
|
||||||
|
|
||||||
|
# Follow live deployment
|
||||||
|
tail -f ansible/logs/deploy_*.log
|
||||||
|
|
||||||
|
# Search logs
|
||||||
|
grep "error\|fail" ansible/logs/*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### SSH Connection Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test SSH manually
|
||||||
|
ssh -i ~/.ssh/id_rsa pi@192.168.1.20
|
||||||
|
|
||||||
|
# Test with verbose output
|
||||||
|
ansible device_1 -i inventory.ini -m ping -vvv
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ansible Module Errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with increased verbosity
|
||||||
|
ansible-playbook deploy.yml -vvv
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device Not Found in Inventory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all hosts
|
||||||
|
ansible all -i inventory.ini --list-hosts
|
||||||
|
|
||||||
|
# List specific group
|
||||||
|
ansible prezenta_devices -i inventory.ini --list-hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **SSH Keys**: Use SSH key authentication instead of passwords
|
||||||
|
2. **Inventory**: Don't commit inventory.ini with passwords to git
|
||||||
|
3. **Ansible Vault**: Use for sensitive data:
|
||||||
|
```bash
|
||||||
|
ansible-vault create secrets.yml
|
||||||
|
ansible-playbook deploy.yml -e @secrets.yml --ask-vault-pass
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Firewall**: Ensure SSH (port 22) is open on devices
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. **Parallel Execution**: Run playbooks on multiple devices
|
||||||
|
```bash
|
||||||
|
ansible-playbook deploy.yml --forks 5
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Task Caching**: Avoid unnecessary updates
|
||||||
|
```bash
|
||||||
|
# Playbooks already use idempotent operations
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Logging**: Monitor log files for issues
|
||||||
|
```bash
|
||||||
|
# Compress old logs
|
||||||
|
gzip ansible/logs/deploy_*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from HTTP Commands
|
||||||
|
|
||||||
|
**Old (HTTP-based):**
|
||||||
|
```python
|
||||||
|
def execute_command_on_device(device_ip, command):
|
||||||
|
url = f"http://{device_ip}:80/execute_command"
|
||||||
|
response = requests.post(url, json={"command": command})
|
||||||
|
return response.json()
|
||||||
|
```
|
||||||
|
|
||||||
|
**New (Ansible-based):**
|
||||||
|
```python
|
||||||
|
from utils_ansible import execute_command_on_device
|
||||||
|
|
||||||
|
result = execute_command_on_device('device_hostname', command)
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Install Ansible on Server_Monitorizare
|
||||||
|
2. Configure inventory.ini with your devices
|
||||||
|
3. Set up SSH key authentication
|
||||||
|
4. Test connectivity with `ansible all -m ping`
|
||||||
|
5. Create first deployment
|
||||||
|
6. Update Flask routes to use new utils_ansible
|
||||||
|
7. Monitor logs in `ansible/logs/`
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For more information on Ansible:
|
||||||
|
- [Ansible Documentation](https://docs.ansible.com/)
|
||||||
|
- [Playbook Syntax](https://docs.ansible.com/ansible/latest/reference_appendices/playbooks_keywords.html)
|
||||||
|
- [Modules Reference](https://docs.ansible.com/ansible/latest/modules/modules_by_category.html)
|
||||||
51
ansible/commands.yml
Normal file
51
ansible/commands.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
# commands.yml - Execute commands on Prezenta devices
|
||||||
|
# Provides structured command execution with logging and error handling
|
||||||
|
|
||||||
|
- name: Execute Command on Prezenta Devices
|
||||||
|
hosts: "{{ target_devices | default('prezenta_devices') }}"
|
||||||
|
gather_facts: no
|
||||||
|
|
||||||
|
vars:
|
||||||
|
command_to_run: "{{ command | default('pwd') }}"
|
||||||
|
command_user: pi
|
||||||
|
log_commands: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Display command execution info
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
Executing on: {{ inventory_hostname }} ({{ ansible_host }})
|
||||||
|
Command: {{ command_to_run }}
|
||||||
|
User: {{ command_user }}
|
||||||
|
|
||||||
|
- name: Execute command
|
||||||
|
shell: "{{ command_to_run }}"
|
||||||
|
register: command_result
|
||||||
|
become: yes
|
||||||
|
become_user: "{{ command_user }}"
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: Display command output
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
Return Code: {{ command_result.rc }}
|
||||||
|
STDOUT:
|
||||||
|
{{ command_result.stdout }}
|
||||||
|
STDERR:
|
||||||
|
{{ command_result.stderr | default('None') }}
|
||||||
|
|
||||||
|
- name: Log command execution
|
||||||
|
lineinfile:
|
||||||
|
path: "{{ app_directory }}/data/command_history.log"
|
||||||
|
line: "[{{ ansible_date_time.iso8601 }}] [{{ inventory_hostname }}] Command: {{ command_to_run }} | RC: {{ command_result.rc }}"
|
||||||
|
create: yes
|
||||||
|
state: present
|
||||||
|
delegate_to: "{{ inventory_hostname }}"
|
||||||
|
become: yes
|
||||||
|
when: log_commands
|
||||||
|
|
||||||
|
- name: Fail if command failed
|
||||||
|
fail:
|
||||||
|
msg: "Command failed on {{ inventory_hostname }} with return code {{ command_result.rc }}"
|
||||||
|
when: command_result.rc != 0 and fail_on_error | default(false)
|
||||||
201
ansible/deploy.yml
Normal file
201
ansible/deploy.yml
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
---
|
||||||
|
# deploy.yml - Deploy updates to Prezenta Work devices
|
||||||
|
# Handles pulling latest code and restarting services
|
||||||
|
|
||||||
|
- name: Deploy Prezenta Work Updates
|
||||||
|
hosts: prezenta_devices
|
||||||
|
gather_facts: yes
|
||||||
|
serial: 1 # Deploy one device at a time to avoid service interruption
|
||||||
|
|
||||||
|
vars:
|
||||||
|
app_directory: "/srv/prezenta_work"
|
||||||
|
git_branch: "dev"
|
||||||
|
restart_service: true
|
||||||
|
backup_before_deploy: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Display deployment information
|
||||||
|
debug:
|
||||||
|
msg: "Deploying to {{ inventory_hostname }} ({{ ansible_host }})"
|
||||||
|
|
||||||
|
# Pre-deployment checks
|
||||||
|
- name: Check if app directory exists
|
||||||
|
stat:
|
||||||
|
path: "{{ app_directory }}"
|
||||||
|
register: app_dir_stat
|
||||||
|
|
||||||
|
- name: Fail if app directory doesn't exist
|
||||||
|
fail:
|
||||||
|
msg: "Application directory {{ app_directory }} not found on {{ inventory_hostname }}"
|
||||||
|
when: not app_dir_stat.stat.exists
|
||||||
|
|
||||||
|
# Backup current code
|
||||||
|
- name: Create backup directory
|
||||||
|
file:
|
||||||
|
path: "{{ app_directory }}/backups"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
when: backup_before_deploy
|
||||||
|
|
||||||
|
- name: Backup current code
|
||||||
|
shell: |
|
||||||
|
cd {{ app_directory }}
|
||||||
|
tar -czf backups/backup_{{ ansible_date_time.iso8601_basic_short }}.tar.gz \
|
||||||
|
--exclude=.git \
|
||||||
|
--exclude=__pycache__ \
|
||||||
|
--exclude=data \
|
||||||
|
--exclude=Files \
|
||||||
|
.
|
||||||
|
register: backup_result
|
||||||
|
when: backup_before_deploy
|
||||||
|
|
||||||
|
- name: Display backup created
|
||||||
|
debug:
|
||||||
|
msg: "Backup created: {{ backup_result.stdout_lines }}"
|
||||||
|
when: backup_before_deploy and backup_result is not skipped
|
||||||
|
|
||||||
|
# Pull latest code
|
||||||
|
- name: Fetch latest from repository
|
||||||
|
shell: |
|
||||||
|
cd {{ app_directory }}
|
||||||
|
git fetch origin
|
||||||
|
register: git_fetch
|
||||||
|
changed_when: "'Fetching' in git_fetch.stdout or 'Receiving' in git_fetch.stdout"
|
||||||
|
|
||||||
|
- name: Checkout dev branch
|
||||||
|
shell: |
|
||||||
|
cd {{ app_directory }}
|
||||||
|
git checkout {{ git_branch }}
|
||||||
|
register: git_checkout
|
||||||
|
|
||||||
|
- name: Pull latest changes
|
||||||
|
shell: |
|
||||||
|
cd {{ app_directory }}
|
||||||
|
git pull origin {{ git_branch }}
|
||||||
|
register: git_pull
|
||||||
|
changed_when: "'Already up to date' not in git_pull.stdout"
|
||||||
|
|
||||||
|
- name: Display git pull result
|
||||||
|
debug:
|
||||||
|
msg: "{{ git_pull.stdout }}"
|
||||||
|
|
||||||
|
# Verify deployment
|
||||||
|
- name: Check Python syntax
|
||||||
|
shell: |
|
||||||
|
python3 -m py_compile {{ app_directory }}/app.py
|
||||||
|
register: syntax_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: syntax_check.rc != 0
|
||||||
|
|
||||||
|
- name: Verify all modules compile
|
||||||
|
shell: |
|
||||||
|
cd {{ app_directory }}
|
||||||
|
python3 -m py_compile *.py
|
||||||
|
register: module_check
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Verify configuration
|
||||||
|
shell: |
|
||||||
|
cd {{ app_directory }}
|
||||||
|
python3 -c "import config_settings; print('✓ Configuration OK')"
|
||||||
|
register: config_check
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Display verification results
|
||||||
|
debug:
|
||||||
|
msg: "{{ config_check.stdout }}"
|
||||||
|
|
||||||
|
# Restart application
|
||||||
|
- name: Restart Prezenta application
|
||||||
|
block:
|
||||||
|
- name: Stop Prezenta service
|
||||||
|
systemd:
|
||||||
|
name: prezenta
|
||||||
|
state: stopped
|
||||||
|
daemon_reload: yes
|
||||||
|
become: yes
|
||||||
|
when: restart_service
|
||||||
|
|
||||||
|
- name: Wait for service to stop
|
||||||
|
pause:
|
||||||
|
seconds: 2
|
||||||
|
|
||||||
|
- name: Start Prezenta service
|
||||||
|
systemd:
|
||||||
|
name: prezenta
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
become: yes
|
||||||
|
when: restart_service
|
||||||
|
|
||||||
|
- name: Verify service is running
|
||||||
|
systemd:
|
||||||
|
name: prezenta
|
||||||
|
state: started
|
||||||
|
become: yes
|
||||||
|
register: service_status
|
||||||
|
until: service_status.status.ActiveState == "active"
|
||||||
|
retries: 3
|
||||||
|
delay: 5
|
||||||
|
rescue:
|
||||||
|
- name: Service restart failed, attempting manual restart
|
||||||
|
debug:
|
||||||
|
msg: "Attempting to restart application manually on {{ inventory_hostname }}"
|
||||||
|
|
||||||
|
- name: Kill existing processes
|
||||||
|
shell: |
|
||||||
|
pkill -f "python3.*app.py" || true
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
- name: Wait before restart
|
||||||
|
pause:
|
||||||
|
seconds: 3
|
||||||
|
|
||||||
|
- name: Start application in background
|
||||||
|
shell: |
|
||||||
|
cd {{ app_directory }}
|
||||||
|
nohup python3 app.py > data/startup.log 2>&1 &
|
||||||
|
become: yes
|
||||||
|
become_user: pi
|
||||||
|
|
||||||
|
# Post-deployment verification
|
||||||
|
- name: Wait for application to start
|
||||||
|
pause:
|
||||||
|
seconds: 5
|
||||||
|
|
||||||
|
- name: Check application status via HTTP
|
||||||
|
uri:
|
||||||
|
url: "http://{{ ansible_host }}:80/status"
|
||||||
|
method: GET
|
||||||
|
status_code: [200, 404]
|
||||||
|
timeout: 10
|
||||||
|
register: app_status
|
||||||
|
retries: 3
|
||||||
|
delay: 2
|
||||||
|
until: app_status.status in [200, 404]
|
||||||
|
|
||||||
|
- name: Display application status
|
||||||
|
debug:
|
||||||
|
msg: "Application on {{ inventory_hostname }} is running"
|
||||||
|
when: app_status.status in [200, 404]
|
||||||
|
|
||||||
|
# Log deployment
|
||||||
|
- name: Record deployment in log
|
||||||
|
lineinfile:
|
||||||
|
path: "{{ app_directory }}/data/deploy.log"
|
||||||
|
line: "[{{ ansible_date_time.iso8601 }}] Deployed {{ git_branch }} from {{ ansible_user }}@monitoring_server"
|
||||||
|
create: yes
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Log deployment summary
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
Deployment completed for {{ inventory_hostname }}
|
||||||
|
Branch: {{ git_branch }}
|
||||||
|
Status: SUCCESS
|
||||||
|
Last git commit: {{ git_pull.stdout_lines[-1] if git_pull.stdout_lines else 'Unknown' }}
|
||||||
|
|
||||||
|
post_tasks:
|
||||||
|
- name: Send deployment notification
|
||||||
|
debug:
|
||||||
|
msg: "Deployment notification: {{ inventory_hostname }} updated to {{ git_branch }}"
|
||||||
34
ansible/inventory.ini
Normal file
34
ansible/inventory.ini
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Ansible Inventory for Prezenta Work Devices
|
||||||
|
# This inventory file manages all Raspberry Pi devices running prezenta_work
|
||||||
|
|
||||||
|
[all:vars]
|
||||||
|
# Common variables for all devices
|
||||||
|
ansible_user=pi
|
||||||
|
ansible_password=Initial01!
|
||||||
|
ansible_become=yes
|
||||||
|
ansible_become_method=sudo
|
||||||
|
ansible_become_password=Initial01!
|
||||||
|
ansible_python_interpreter=/usr/bin/python3
|
||||||
|
ansible_connection=ssh
|
||||||
|
|
||||||
|
[prezenta_devices]
|
||||||
|
# Raspberry Pi devices running the prezenta_work application
|
||||||
|
# Format: device_name ansible_host=192.168.1.x
|
||||||
|
|
||||||
|
device_1 ansible_host=192.168.1.20
|
||||||
|
device_2 ansible_host=192.168.1.21
|
||||||
|
device_3 ansible_host=192.168.1.22
|
||||||
|
|
||||||
|
[prezenta_devices:vars]
|
||||||
|
# Variables specific to prezenta devices
|
||||||
|
app_directory=/srv/prezenta_work
|
||||||
|
app_user=pi
|
||||||
|
app_service=prezenta
|
||||||
|
|
||||||
|
[monitoring_server]
|
||||||
|
# Central monitoring server
|
||||||
|
monitor ansible_host=192.168.1.103 ansible_user=root
|
||||||
|
|
||||||
|
[local]
|
||||||
|
# Local development/test
|
||||||
|
localhost ansible_connection=local ansible_become=no
|
||||||
64
ansible/requirements.txt
Normal file
64
ansible/requirements.txt
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Requirements for Server_Monitorizare Ansible Integration
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
- Ubuntu/Debian Linux
|
||||||
|
- Python 3.7+
|
||||||
|
- SSH access to target devices
|
||||||
|
|
||||||
|
## Python Packages
|
||||||
|
# Flask and server dependencies
|
||||||
|
Flask==2.3.0
|
||||||
|
Werkzeug==2.3.0
|
||||||
|
requests==2.31.0
|
||||||
|
|
||||||
|
# Ansible automation
|
||||||
|
ansible>=2.10.0,<3.0.0
|
||||||
|
ansible-core>=2.12.0
|
||||||
|
|
||||||
|
# Additional utilities
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
pyyaml==6.0
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Install System Dependencies
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y python3-pip python3-dev
|
||||||
|
sudo apt-get install -y openssh-client openssh-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Python Packages
|
||||||
|
```bash
|
||||||
|
cd /srv/Server_Monitorizare
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Install Ansible
|
||||||
|
```bash
|
||||||
|
pip3 install ansible>=2.10.0
|
||||||
|
# or
|
||||||
|
sudo apt-get install -y ansible
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verify Installation
|
||||||
|
```bash
|
||||||
|
ansible --version
|
||||||
|
ansible-playbook --version
|
||||||
|
python3 -c "import ansible; print(ansible.__version__)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional: For SSH Key Authentication
|
||||||
|
```bash
|
||||||
|
sudo apt-get install -y openssh-client
|
||||||
|
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
```bash
|
||||||
|
# Test Ansible installation
|
||||||
|
ansible localhost -m debug -a msg="Ansible works!"
|
||||||
|
|
||||||
|
# Test against devices
|
||||||
|
ansible all -i ansible/inventory.ini -m ping
|
||||||
|
```
|
||||||
163
ansible/system_update.yml
Normal file
163
ansible/system_update.yml
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
# system_update.yml - System updates and maintenance
|
||||||
|
# Updates OS packages, manages services, and performs health checks
|
||||||
|
|
||||||
|
- name: System Update and Maintenance
|
||||||
|
hosts: "{{ target_devices | default('prezenta_devices') }}"
|
||||||
|
serial: 1 # One device at a time to maintain availability
|
||||||
|
gather_facts: yes
|
||||||
|
|
||||||
|
vars:
|
||||||
|
update_os_packages: false
|
||||||
|
update_python_packages: true
|
||||||
|
perform_health_check: true
|
||||||
|
reboot_after_update: false
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
# System Information
|
||||||
|
- name: Gather system information
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
System: {{ ansible_system }}
|
||||||
|
Distribution: {{ ansible_distribution }} {{ ansible_distribution_version }}
|
||||||
|
Hostname: {{ ansible_hostname }}
|
||||||
|
IP Address: {{ ansible_default_ipv4.address }}
|
||||||
|
Uptime: {{ ansible_uptime_seconds }} seconds
|
||||||
|
|
||||||
|
# OS Package Updates
|
||||||
|
- name: Update OS package lists
|
||||||
|
apt:
|
||||||
|
update_cache: yes
|
||||||
|
cache_valid_time: 300
|
||||||
|
become: yes
|
||||||
|
when: update_os_packages
|
||||||
|
|
||||||
|
- name: Upgrade OS packages
|
||||||
|
apt:
|
||||||
|
upgrade: full
|
||||||
|
autoremove: yes
|
||||||
|
autoclean: yes
|
||||||
|
become: yes
|
||||||
|
register: apt_upgrade
|
||||||
|
when: update_os_packages
|
||||||
|
|
||||||
|
- name: Display OS updates
|
||||||
|
debug:
|
||||||
|
msg: "OS packages updated"
|
||||||
|
when: update_os_packages and apt_upgrade.changed
|
||||||
|
|
||||||
|
# Python Package Updates
|
||||||
|
- name: Check for prezenta_work directory
|
||||||
|
stat:
|
||||||
|
path: "{{ app_directory }}"
|
||||||
|
register: app_dir
|
||||||
|
|
||||||
|
- name: Update Python dependencies
|
||||||
|
block:
|
||||||
|
- name: Find requirements.txt
|
||||||
|
stat:
|
||||||
|
path: "{{ app_directory }}/requirements.txt"
|
||||||
|
register: requirements_file
|
||||||
|
|
||||||
|
- name: Install Python requirements
|
||||||
|
pip:
|
||||||
|
requirements: "{{ app_directory }}/requirements.txt"
|
||||||
|
state: latest
|
||||||
|
become: yes
|
||||||
|
when: requirements_file.stat.exists
|
||||||
|
|
||||||
|
- name: Install Flask if not present
|
||||||
|
pip:
|
||||||
|
name:
|
||||||
|
- Flask
|
||||||
|
- requests
|
||||||
|
- RPi.GPIO
|
||||||
|
state: latest
|
||||||
|
become: yes
|
||||||
|
register: pip_install
|
||||||
|
|
||||||
|
- name: Display Python updates
|
||||||
|
debug:
|
||||||
|
msg: "Python packages updated"
|
||||||
|
when: pip_install.changed
|
||||||
|
when: app_dir.stat.exists and update_python_packages
|
||||||
|
|
||||||
|
# Service Management
|
||||||
|
- name: Check Prezenta service status
|
||||||
|
systemd:
|
||||||
|
name: prezenta
|
||||||
|
enabled: yes
|
||||||
|
become: yes
|
||||||
|
register: prezenta_service
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: Display service status
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
Service: {{ prezenta_service.status.ActiveState if prezenta_service.status is defined else 'Not found' }}
|
||||||
|
Enabled: {{ prezenta_service.status.UnitFileState if prezenta_service.status is defined else 'Unknown' }}
|
||||||
|
|
||||||
|
# Health Checks
|
||||||
|
- name: Check disk space
|
||||||
|
shell: df -h / | tail -1 | awk '{print $5}'
|
||||||
|
register: disk_usage
|
||||||
|
changed_when: false
|
||||||
|
when: perform_health_check
|
||||||
|
|
||||||
|
- name: Check memory usage
|
||||||
|
shell: free -h | grep Mem | awk '{print $3 "/" $2}'
|
||||||
|
register: mem_usage
|
||||||
|
changed_when: false
|
||||||
|
when: perform_health_check
|
||||||
|
|
||||||
|
- name: Check CPU temperature (Raspberry Pi)
|
||||||
|
shell: vcgencmd measure_temp 2>/dev/null | grep -oP '\d+\.\d+' || echo "N/A"
|
||||||
|
register: cpu_temp
|
||||||
|
changed_when: false
|
||||||
|
when: perform_health_check and ansible_system == 'Linux'
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: Display health check results
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
Disk Usage: {{ disk_usage.stdout }}
|
||||||
|
Memory Usage: {{ mem_usage.stdout }}
|
||||||
|
CPU Temp: {{ cpu_temp.stdout if cpu_temp.stdout != 'N/A' else 'N/A' }}°C
|
||||||
|
when: perform_health_check
|
||||||
|
|
||||||
|
- name: Warn if disk space critical
|
||||||
|
debug:
|
||||||
|
msg: "WARNING: Disk usage is {{ disk_usage.stdout }} - Consider cleanup"
|
||||||
|
when:
|
||||||
|
- perform_health_check
|
||||||
|
- disk_usage.stdout | int >= 85
|
||||||
|
|
||||||
|
# Log update
|
||||||
|
- name: Create system update log
|
||||||
|
lineinfile:
|
||||||
|
path: "{{ app_directory }}/data/system_update.log"
|
||||||
|
line: "[{{ ansible_date_time.iso8601 }}] System maintenance completed - Disk: {{ disk_usage.stdout }} | Memory: {{ mem_usage.stdout }}"
|
||||||
|
create: yes
|
||||||
|
state: present
|
||||||
|
become: yes
|
||||||
|
when: perform_health_check and app_dir.stat.exists
|
||||||
|
|
||||||
|
# Reboot if required
|
||||||
|
- name: Schedule reboot if needed
|
||||||
|
debug:
|
||||||
|
msg: "System reboot scheduled after updates"
|
||||||
|
when: reboot_after_update and apt_upgrade.changed
|
||||||
|
|
||||||
|
- name: Reboot system
|
||||||
|
reboot:
|
||||||
|
msg: "Rebooting after system updates"
|
||||||
|
pre_reboot_delay: 60
|
||||||
|
become: yes
|
||||||
|
when: reboot_after_update and apt_upgrade.changed
|
||||||
|
|
||||||
|
post_tasks:
|
||||||
|
- name: Display maintenance summary
|
||||||
|
debug:
|
||||||
|
msg: |
|
||||||
|
Maintenance completed for {{ inventory_hostname }}
|
||||||
|
Date: {{ ansible_date_time.iso8601 }}
|
||||||
425
ansible_integration.py
Normal file
425
ansible_integration.py
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
"""
|
||||||
|
Ansible Integration Module for Server_Monitorizare
|
||||||
|
Provides functions to execute Ansible playbooks and commands on remote devices
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Any, Optional, Tuple
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleManager:
|
||||||
|
"""Manager class for Ansible operations"""
|
||||||
|
|
||||||
|
def __init__(self, ansible_dir: str = None, inventory_file: str = None):
|
||||||
|
"""
|
||||||
|
Initialize Ansible manager
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ansible_dir: Path to ansible directory
|
||||||
|
inventory_file: Path to inventory file
|
||||||
|
"""
|
||||||
|
self.ansible_dir = ansible_dir or os.path.join(
|
||||||
|
os.path.dirname(__file__), 'ansible'
|
||||||
|
)
|
||||||
|
self.inventory_file = inventory_file or os.path.join(
|
||||||
|
self.ansible_dir, 'inventory.ini'
|
||||||
|
)
|
||||||
|
self.log_dir = os.path.join(self.ansible_dir, 'logs')
|
||||||
|
|
||||||
|
# Create logs directory if it doesn't exist
|
||||||
|
Path(self.log_dir).mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def _verify_ansible_installed(self) -> bool:
|
||||||
|
"""Verify Ansible is installed"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['ansible', '--version'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ansible not found: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def deploy_to_devices(
|
||||||
|
self,
|
||||||
|
target_hosts: str = 'prezenta_devices',
|
||||||
|
git_branch: str = 'dev',
|
||||||
|
backup: bool = True,
|
||||||
|
restart: bool = True
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Deploy updates to devices using deploy.yml playbook
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_hosts: Target host group or specific host
|
||||||
|
git_branch: Git branch to deploy
|
||||||
|
backup: Whether to backup before deploy
|
||||||
|
restart: Whether to restart service
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Execution result with status and output
|
||||||
|
"""
|
||||||
|
if not self._verify_ansible_installed():
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': 'Ansible is not installed',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
playbook_file = os.path.join(self.ansible_dir, 'deploy.yml')
|
||||||
|
if not os.path.exists(playbook_file):
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'Playbook not found: {playbook_file}',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare Ansible command
|
||||||
|
cmd = [
|
||||||
|
'ansible-playbook',
|
||||||
|
'-i', self.inventory_file,
|
||||||
|
playbook_file,
|
||||||
|
'-e', f'git_branch={git_branch}',
|
||||||
|
'-e', f'backup_before_deploy={str(backup).lower()}',
|
||||||
|
'-e', f'restart_service={str(restart).lower()}',
|
||||||
|
'--limit', target_hosts,
|
||||||
|
'-v'
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(f"Executing deployment: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
log_file = os.path.join(
|
||||||
|
self.log_dir,
|
||||||
|
f'deploy_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_file, 'w') as log:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=600, # 10 minute timeout
|
||||||
|
cwd=self.ansible_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
log.write(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
log.write("\n--- STDERR ---\n")
|
||||||
|
log.write(result.stderr)
|
||||||
|
|
||||||
|
success = result.returncode == 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'returncode': result.returncode,
|
||||||
|
'output': result.stdout,
|
||||||
|
'error': result.stderr if not success else None,
|
||||||
|
'log_file': log_file,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'target_hosts': target_hosts,
|
||||||
|
'git_branch': git_branch
|
||||||
|
}
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': 'Deployment timed out after 10 minutes',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Deployment failed: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'Deployment failed: {str(e)}',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute_command_on_device(
|
||||||
|
self,
|
||||||
|
device: str,
|
||||||
|
command: str,
|
||||||
|
user: str = 'pi'
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute command on specific device
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device: Device hostname or IP
|
||||||
|
command: Command to execute
|
||||||
|
user: User to execute command as
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Execution result
|
||||||
|
"""
|
||||||
|
if not self._verify_ansible_installed():
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': 'Ansible is not installed',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
playbook_file = os.path.join(self.ansible_dir, 'commands.yml')
|
||||||
|
if not os.path.exists(playbook_file):
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'Playbook not found: {playbook_file}',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'ansible-playbook',
|
||||||
|
'-i', self.inventory_file,
|
||||||
|
playbook_file,
|
||||||
|
'--limit', device,
|
||||||
|
'-e', f'command={command}',
|
||||||
|
'-e', f'command_user={user}',
|
||||||
|
'-v'
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(f"Executing command on {device}: {command}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
log_file = os.path.join(
|
||||||
|
self.log_dir,
|
||||||
|
f'command_{device}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_file, 'w') as log:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
cwd=self.ansible_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
log.write(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
log.write("\n--- STDERR ---\n")
|
||||||
|
log.write(result.stderr)
|
||||||
|
|
||||||
|
success = result.returncode == 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'returncode': result.returncode,
|
||||||
|
'output': result.stdout,
|
||||||
|
'error': result.stderr if not success else None,
|
||||||
|
'log_file': log_file,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'device': device,
|
||||||
|
'command': command
|
||||||
|
}
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': 'Command execution timed out after 60 seconds',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Command execution failed: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'Command execution failed: {str(e)}',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
def system_update(
|
||||||
|
self,
|
||||||
|
target_hosts: str = 'prezenta_devices',
|
||||||
|
update_os: bool = False,
|
||||||
|
update_python: bool = True,
|
||||||
|
health_check: bool = True
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute system update and maintenance
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_hosts: Target host group
|
||||||
|
update_os: Whether to update OS packages
|
||||||
|
update_python: Whether to update Python packages
|
||||||
|
health_check: Whether to perform health check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Execution result
|
||||||
|
"""
|
||||||
|
if not self._verify_ansible_installed():
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': 'Ansible is not installed',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
playbook_file = os.path.join(self.ansible_dir, 'system_update.yml')
|
||||||
|
if not os.path.exists(playbook_file):
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'Playbook not found: {playbook_file}',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'ansible-playbook',
|
||||||
|
'-i', self.inventory_file,
|
||||||
|
playbook_file,
|
||||||
|
'--limit', target_hosts,
|
||||||
|
'-e', f'update_os_packages={str(update_os).lower()}',
|
||||||
|
'-e', f'update_python_packages={str(update_python).lower()}',
|
||||||
|
'-e', f'perform_health_check={str(health_check).lower()}',
|
||||||
|
'-v'
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(f"Executing system update on {target_hosts}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
log_file = os.path.join(
|
||||||
|
self.log_dir,
|
||||||
|
f'system_update_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_file, 'w') as log:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=1800, # 30 minute timeout
|
||||||
|
cwd=self.ansible_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
log.write(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
log.write("\n--- STDERR ---\n")
|
||||||
|
log.write(result.stderr)
|
||||||
|
|
||||||
|
success = result.returncode == 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'returncode': result.returncode,
|
||||||
|
'output': result.stdout,
|
||||||
|
'error': result.stderr if not success else None,
|
||||||
|
'log_file': log_file,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'target_hosts': target_hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': 'System update timed out after 30 minutes',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"System update failed: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'System update failed: {str(e)}',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_device_facts(self, device: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get facts about a specific device
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device: Device hostname or IP
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Device facts and information
|
||||||
|
"""
|
||||||
|
if not self._verify_ansible_installed():
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': 'Ansible is not installed'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'ansible',
|
||||||
|
device,
|
||||||
|
'-i', self.inventory_file,
|
||||||
|
'-m', 'setup'
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
cwd=self.ansible_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Parse JSON output
|
||||||
|
output_lines = result.stdout.split('\n')
|
||||||
|
for line in output_lines:
|
||||||
|
if line.startswith(device):
|
||||||
|
try:
|
||||||
|
data = json.loads(line.split(' => ', 1)[1])
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'device': device,
|
||||||
|
'facts': data.get('ansible_facts', {}),
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'Failed to get facts for {device}',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Failed to get device facts: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'Failed to get device facts: {str(e)}',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_execution_logs(self, limit: int = 10) -> List[Dict[str, str]]:
|
||||||
|
"""Get recent execution logs"""
|
||||||
|
logs = []
|
||||||
|
try:
|
||||||
|
log_files = sorted(
|
||||||
|
Path(self.log_dir).glob('*.log'),
|
||||||
|
key=os.path.getmtime,
|
||||||
|
reverse=True
|
||||||
|
)[:limit]
|
||||||
|
|
||||||
|
for log_file in log_files:
|
||||||
|
logs.append({
|
||||||
|
'filename': log_file.name,
|
||||||
|
'timestamp': datetime.fromtimestamp(log_file.stat().st_mtime).isoformat(),
|
||||||
|
'size': log_file.stat().st_size
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get execution logs: {e}")
|
||||||
|
|
||||||
|
return logs
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
ansible_manager = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_ansible_manager(ansible_dir: str = None) -> AnsibleManager:
|
||||||
|
"""Get or create global Ansible manager instance"""
|
||||||
|
global ansible_manager
|
||||||
|
if ansible_manager is None:
|
||||||
|
ansible_manager = AnsibleManager(ansible_dir)
|
||||||
|
return ansible_manager
|
||||||
481
utils_ansible.py
Normal file
481
utils_ansible.py
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
"""
|
||||||
|
Updated Utility Functions for Server_Monitorizare
|
||||||
|
Now using Ansible for remote device management instead of direct HTTP commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
from flask import jsonify, request, session
|
||||||
|
from datetime import datetime
|
||||||
|
from ansible_integration import get_ansible_manager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(Exception):
|
||||||
|
"""Custom exception for API errors"""
|
||||||
|
def __init__(self, message, status_code=400, details=None):
|
||||||
|
self.message = message
|
||||||
|
self.status_code = status_code
|
||||||
|
self.details = details or {}
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
def error_response(error_message, status_code=400, details=None):
|
||||||
|
"""Create a standardized error response"""
|
||||||
|
response = {
|
||||||
|
'success': False,
|
||||||
|
'error': error_message,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
if details:
|
||||||
|
response['details'] = details
|
||||||
|
return jsonify(response), status_code
|
||||||
|
|
||||||
|
|
||||||
|
def success_response(data=None, message="Success", status_code=200):
|
||||||
|
"""Create a standardized success response"""
|
||||||
|
response = {
|
||||||
|
'success': True,
|
||||||
|
'message': message,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
response['data'] = data
|
||||||
|
return jsonify(response), status_code
|
||||||
|
|
||||||
|
|
||||||
|
def require_auth(f):
|
||||||
|
"""Decorator to require authentication"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
# Check for API key in headers
|
||||||
|
api_key = request.headers.get('X-API-Key')
|
||||||
|
from config import get_config
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
if not api_key or api_key != config.API_KEY:
|
||||||
|
logger.warning(f"Unauthorized access attempt from {request.remote_addr}")
|
||||||
|
return error_response("Unauthorized", 401)
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def require_session_auth(f):
|
||||||
|
"""Decorator to require session-based authentication"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return error_response("Authentication required", 401)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def log_request(f):
|
||||||
|
"""Decorator to log request details"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
logger.info(f"{request.method} {request.path} from {request.remote_addr}")
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
except APIError as e:
|
||||||
|
logger.error(f"API Error in {f.__name__}: {e.message}")
|
||||||
|
return error_response(e.message, e.status_code, e.details)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Unexpected error in {f.__name__}")
|
||||||
|
return error_response("Internal server error", 500)
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def validate_required_fields(required_fields):
|
||||||
|
"""Decorator to validate required fields in JSON request"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not request.is_json:
|
||||||
|
return error_response("Content-Type must be application/json", 400)
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
missing_fields = [field for field in required_fields if field not in data or not data[field]]
|
||||||
|
|
||||||
|
if missing_fields:
|
||||||
|
return error_response(
|
||||||
|
"Missing required fields",
|
||||||
|
400,
|
||||||
|
{'missing_fields': missing_fields}
|
||||||
|
)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ip_address(ip):
|
||||||
|
"""Validate IP address format"""
|
||||||
|
import re
|
||||||
|
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
|
||||||
|
if not re.match(pattern, ip):
|
||||||
|
return False
|
||||||
|
parts = ip.split('.')
|
||||||
|
return all(0 <= int(part) <= 255 for part in parts)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_hostname(hostname):
|
||||||
|
"""Sanitize hostname to prevent injection"""
|
||||||
|
import re
|
||||||
|
# Allow alphanumeric, dash, and underscore
|
||||||
|
if not re.match(r'^[a-zA-Z0-9_-]+$', hostname):
|
||||||
|
raise APIError("Invalid hostname format", 400)
|
||||||
|
if len(hostname) > 255:
|
||||||
|
raise APIError("Hostname too long", 400)
|
||||||
|
return hostname
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(config):
|
||||||
|
"""Setup logging configuration"""
|
||||||
|
import os
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
|
# Create logs directory if it doesn't exist
|
||||||
|
log_dir = os.path.dirname(config.LOG_FILE)
|
||||||
|
if log_dir and not os.path.exists(log_dir):
|
||||||
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
|
# Create logger
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(getattr(logging, config.LOG_LEVEL))
|
||||||
|
|
||||||
|
# File handler with rotation
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
config.LOG_FILE,
|
||||||
|
maxBytes=config.LOG_MAX_BYTES,
|
||||||
|
backupCount=config.LOG_BACKUP_COUNT
|
||||||
|
)
|
||||||
|
file_handler.setFormatter(logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
))
|
||||||
|
|
||||||
|
# Console handler
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setFormatter(logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
))
|
||||||
|
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ANSIBLE-BASED DEVICE MANAGEMENT FUNCTIONS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def execute_command_on_device(device_hostname: str, command: str, user: str = 'pi'):
|
||||||
|
"""
|
||||||
|
Execute command on a device using Ansible
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_hostname: Device hostname from inventory
|
||||||
|
command: Command to execute
|
||||||
|
user: User to execute command as
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Execution result with success status and output
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ansible_mgr = get_ansible_manager()
|
||||||
|
|
||||||
|
logger.info(f"Executing command on {device_hostname}: {command}")
|
||||||
|
|
||||||
|
result = ansible_mgr.execute_command_on_device(
|
||||||
|
device=device_hostname,
|
||||||
|
command=command,
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
logger.info(f"Command executed successfully on {device_hostname}")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"result": result['output'],
|
||||||
|
"device": device_hostname,
|
||||||
|
"timestamp": result['timestamp']
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logger.error(f"Command failed on {device_hostname}: {result.get('error')}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": result.get('error', 'Unknown error'),
|
||||||
|
"device": device_hostname,
|
||||||
|
"timestamp": result['timestamp']
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error executing command on {device_hostname}: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Connection error: {str(e)}",
|
||||||
|
"device": device_hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_updates_to_device(
|
||||||
|
device_hostname: str,
|
||||||
|
git_branch: str = 'dev',
|
||||||
|
backup: bool = True,
|
||||||
|
restart: bool = True
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Deploy updates to device using Ansible deploy playbook
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_hostname: Device hostname from inventory
|
||||||
|
git_branch: Git branch to deploy
|
||||||
|
backup: Whether to backup before deploy
|
||||||
|
restart: Whether to restart service
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Deployment result
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ansible_mgr = get_ansible_manager()
|
||||||
|
|
||||||
|
logger.info(f"Starting deployment on {device_hostname} from branch {git_branch}")
|
||||||
|
|
||||||
|
result = ansible_mgr.deploy_to_devices(
|
||||||
|
target_hosts=device_hostname,
|
||||||
|
git_branch=git_branch,
|
||||||
|
backup=backup,
|
||||||
|
restart=restart
|
||||||
|
)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
logger.info(f"Deployment completed successfully on {device_hostname}")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Deployed {git_branch} to {device_hostname}",
|
||||||
|
"device": device_hostname,
|
||||||
|
"branch": git_branch,
|
||||||
|
"log_file": result.get('log_file'),
|
||||||
|
"timestamp": result['timestamp']
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logger.error(f"Deployment failed on {device_hostname}: {result.get('error')}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": result.get('error', 'Deployment failed'),
|
||||||
|
"device": device_hostname,
|
||||||
|
"timestamp": result['timestamp']
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error deploying to {device_hostname}: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Deployment error: {str(e)}",
|
||||||
|
"device": device_hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_updates_to_all_devices(
|
||||||
|
git_branch: str = 'dev',
|
||||||
|
backup: bool = True,
|
||||||
|
restart: bool = True
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Deploy updates to all devices in the 'prezenta_devices' group
|
||||||
|
|
||||||
|
Args:
|
||||||
|
git_branch: Git branch to deploy
|
||||||
|
backup: Whether to backup before deploy
|
||||||
|
restart: Whether to restart service
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Deployment result for all devices
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ansible_mgr = get_ansible_manager()
|
||||||
|
|
||||||
|
logger.info(f"Starting batch deployment to all devices from branch {git_branch}")
|
||||||
|
|
||||||
|
result = ansible_mgr.deploy_to_devices(
|
||||||
|
target_hosts='prezenta_devices',
|
||||||
|
git_branch=git_branch,
|
||||||
|
backup=backup,
|
||||||
|
restart=restart
|
||||||
|
)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
logger.info(f"Batch deployment completed successfully")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"Deployed {git_branch} to all devices",
|
||||||
|
"branch": git_branch,
|
||||||
|
"log_file": result.get('log_file'),
|
||||||
|
"timestamp": result['timestamp']
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logger.error(f"Batch deployment failed: {result.get('error')}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": result.get('error', 'Batch deployment failed'),
|
||||||
|
"timestamp": result['timestamp']
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error in batch deployment: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Batch deployment error: {str(e)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def system_update_device(
|
||||||
|
device_hostname: str,
|
||||||
|
update_os: bool = False,
|
||||||
|
update_python: bool = True,
|
||||||
|
health_check: bool = True
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Perform system update and maintenance on device
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_hostname: Device hostname from inventory
|
||||||
|
update_os: Whether to update OS packages
|
||||||
|
update_python: Whether to update Python packages
|
||||||
|
health_check: Whether to perform health check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Update result
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ansible_mgr = get_ansible_manager()
|
||||||
|
|
||||||
|
logger.info(f"Starting system update on {device_hostname}")
|
||||||
|
|
||||||
|
result = ansible_mgr.system_update(
|
||||||
|
target_hosts=device_hostname,
|
||||||
|
update_os=update_os,
|
||||||
|
update_python=update_python,
|
||||||
|
health_check=health_check
|
||||||
|
)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
logger.info(f"System update completed on {device_hostname}")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"System update completed on {device_hostname}",
|
||||||
|
"device": device_hostname,
|
||||||
|
"log_file": result.get('log_file'),
|
||||||
|
"timestamp": result['timestamp']
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logger.error(f"System update failed on {device_hostname}: {result.get('error')}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": result.get('error', 'System update failed'),
|
||||||
|
"device": device_hostname,
|
||||||
|
"timestamp": result['timestamp']
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error updating system on {device_hostname}: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"System update error: {str(e)}",
|
||||||
|
"device": device_hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_facts(device_hostname: str):
|
||||||
|
"""
|
||||||
|
Get facts about a device using Ansible
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_hostname: Device hostname from inventory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Device facts and information
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ansible_mgr = get_ansible_manager()
|
||||||
|
|
||||||
|
logger.info(f"Gathering facts for {device_hostname}")
|
||||||
|
|
||||||
|
result = ansible_mgr.get_device_facts(device_hostname)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"device": device_hostname,
|
||||||
|
"facts": result.get('facts', {}),
|
||||||
|
"timestamp": result['timestamp']
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to get facts for {device_hostname}: {result.get('error')}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": result.get('error', 'Failed to get device facts'),
|
||||||
|
"device": device_hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error getting facts for {device_hostname}: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Error getting device facts: {str(e)}",
|
||||||
|
"device": device_hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_status(device_hostname: str):
|
||||||
|
"""
|
||||||
|
Get device status (uses get_device_facts internally)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_hostname: Device hostname from inventory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Device status information
|
||||||
|
"""
|
||||||
|
facts = get_device_facts(device_hostname)
|
||||||
|
|
||||||
|
if facts['success']:
|
||||||
|
ansible_facts = facts.get('facts', {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"status": {
|
||||||
|
"hostname": device_hostname,
|
||||||
|
"system": ansible_facts.get('ansible_system'),
|
||||||
|
"distribution": ansible_facts.get('ansible_distribution'),
|
||||||
|
"version": ansible_facts.get('ansible_distribution_version'),
|
||||||
|
"ip_address": ansible_facts.get('ansible_default_ipv4', {}).get('address'),
|
||||||
|
"uptime_seconds": ansible_facts.get('ansible_uptime_seconds'),
|
||||||
|
"processor_count": ansible_facts.get('ansible_processor_count'),
|
||||||
|
"memtotal_mb": ansible_facts.get('ansible_memtotal_mb'),
|
||||||
|
"memfree_mb": ansible_facts.get('ansible_memfree_mb'),
|
||||||
|
"timestamp": facts['timestamp']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return facts
|
||||||
|
|
||||||
|
|
||||||
|
def get_execution_logs(limit: int = 10):
|
||||||
|
"""Get recent Ansible execution logs"""
|
||||||
|
try:
|
||||||
|
ansible_mgr = get_ansible_manager()
|
||||||
|
logs = ansible_mgr.get_execution_logs(limit)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"logs": logs,
|
||||||
|
"count": len(logs)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error getting execution logs: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Error getting logs: {str(e)}"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user