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:
Developer
2025-12-18 13:59:48 +02:00
parent 376240fb06
commit cb52e67afa
8 changed files with 1807 additions and 0 deletions

388
ansible/README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)}"
}