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