Add comprehensive deployment and testing scripts
- deploy-onprem.sh: Automated on-prem deployment script - mock-scada-server.py: Mock SCADA system for testing - mock-optimization-server.py: Mock optimization system for testing - test-e2e-deployment.py: End-to-end deployment testing - validate-deployment.sh: Deployment health validation - DEPLOYMENT_GUIDE.md: Comprehensive deployment documentation Features: - Automated customer deployment with minimal manual steps - Mock systems for testing without real hardware - Comprehensive end-to-end testing - Health validation and monitoring - Production-ready deployment scripts
This commit is contained in:
parent
bac6818946
commit
b76838ea8e
|
|
@ -0,0 +1,296 @@
|
||||||
|
# Calejo Control Adapter - Deployment Guide
|
||||||
|
|
||||||
|
This guide provides comprehensive instructions for deploying the Calejo Control Adapter in on-premises customer environments.
|
||||||
|
|
||||||
|
## 🚀 Quick Deployment
|
||||||
|
|
||||||
|
### Automated Deployment (Recommended)
|
||||||
|
|
||||||
|
For quick and easy deployment, use the automated deployment script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run as root for system-wide installation
|
||||||
|
sudo ./deploy-onprem.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script will:
|
||||||
|
- Check prerequisites (Docker, Docker Compose)
|
||||||
|
- Create necessary directories
|
||||||
|
- Copy all required files
|
||||||
|
- Create systemd service for automatic startup
|
||||||
|
- Build and start all services
|
||||||
|
- Create backup and health check scripts
|
||||||
|
|
||||||
|
### Manual Deployment
|
||||||
|
|
||||||
|
If you prefer manual deployment:
|
||||||
|
|
||||||
|
1. **Install Prerequisites**
|
||||||
|
```bash
|
||||||
|
# Install Docker
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh get-docker.sh
|
||||||
|
|
||||||
|
# Install Docker Compose
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploy Application**
|
||||||
|
```bash
|
||||||
|
# Create directories
|
||||||
|
sudo mkdir -p /opt/calejo-control-adapter
|
||||||
|
sudo mkdir -p /var/log/calejo
|
||||||
|
sudo mkdir -p /etc/calejo
|
||||||
|
sudo mkdir -p /var/backup/calejo
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
sudo cp -r ./* /opt/calejo-control-adapter/
|
||||||
|
sudo cp config/settings.py /etc/calejo/
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
sudo chmod +x /opt/calejo-control-adapter/scripts/*.sh
|
||||||
|
|
||||||
|
# Build and start
|
||||||
|
cd /opt/calejo-control-adapter
|
||||||
|
sudo docker-compose build
|
||||||
|
sudo docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing the Deployment
|
||||||
|
|
||||||
|
### End-to-End Testing
|
||||||
|
|
||||||
|
Test the complete system with mock SCADA and optimization servers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run comprehensive end-to-end tests
|
||||||
|
python test-e2e-deployment.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Start mock SCADA server
|
||||||
|
- Start mock optimization server
|
||||||
|
- Start main application
|
||||||
|
- Test all endpoints and functionality
|
||||||
|
- Validate integration between components
|
||||||
|
|
||||||
|
### Individual Component Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test mock SCADA server
|
||||||
|
python mock-scada-server.py
|
||||||
|
|
||||||
|
# Test mock optimization server
|
||||||
|
python mock-optimization-server.py
|
||||||
|
|
||||||
|
# Test local dashboard functionality
|
||||||
|
python test_dashboard_local.py
|
||||||
|
|
||||||
|
# Test deployment health
|
||||||
|
./validate-deployment.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Deployment Validation
|
||||||
|
|
||||||
|
After deployment, validate that everything is working correctly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run comprehensive validation
|
||||||
|
./validate-deployment.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This checks:
|
||||||
|
- ✅ System resources (disk, memory, CPU)
|
||||||
|
- ✅ Docker container status
|
||||||
|
- ✅ Application endpoints
|
||||||
|
- ✅ Configuration validity
|
||||||
|
- ✅ Log files
|
||||||
|
- ✅ Security configuration
|
||||||
|
- ✅ Backup setup
|
||||||
|
|
||||||
|
## 📊 Mock Systems for Testing
|
||||||
|
|
||||||
|
### Mock SCADA Server
|
||||||
|
|
||||||
|
The mock SCADA server (`mock-scada-server.py`) simulates:
|
||||||
|
- **OPC UA Server** on port 4840
|
||||||
|
- **Modbus TCP Server** on port 502
|
||||||
|
- **Real-time process data** (temperature, pressure, flow, level)
|
||||||
|
- **Historical data trends**
|
||||||
|
- **Alarm simulation**
|
||||||
|
|
||||||
|
### Mock Optimization Server
|
||||||
|
|
||||||
|
The mock optimization server (`mock-optimization-server.py`) simulates:
|
||||||
|
- **Multiple optimization strategies**
|
||||||
|
- **Market data simulation**
|
||||||
|
- **Setpoint calculations**
|
||||||
|
- **Cost and energy savings analysis**
|
||||||
|
- **Confidence scoring**
|
||||||
|
|
||||||
|
## 🔧 Management Commands
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start service
|
||||||
|
sudo systemctl start calejo-control-adapter
|
||||||
|
|
||||||
|
# Stop service
|
||||||
|
sudo systemctl stop calejo-control-adapter
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
sudo systemctl status calejo-control-adapter
|
||||||
|
|
||||||
|
# Enable auto-start
|
||||||
|
sudo systemctl enable calejo-control-adapter
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health check
|
||||||
|
/opt/calejo-control-adapter/scripts/health-check.sh
|
||||||
|
|
||||||
|
# Full backup
|
||||||
|
/opt/calejo-control-adapter/scripts/backup-full.sh
|
||||||
|
|
||||||
|
# Restore from backup
|
||||||
|
/opt/calejo-control-adapter/scripts/restore-full.sh <backup-file>
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
sudo docker-compose logs -f app
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/calejo-control-adapter/ # Main application directory
|
||||||
|
├── src/ # Source code
|
||||||
|
├── static/ # Static files (dashboard)
|
||||||
|
├── config/ # Configuration files
|
||||||
|
├── scripts/ # Management scripts
|
||||||
|
├── monitoring/ # Monitoring configuration
|
||||||
|
├── tests/ # Test files
|
||||||
|
└── docker-compose.yml # Docker Compose configuration
|
||||||
|
|
||||||
|
/var/log/calejo/ # Application logs
|
||||||
|
/etc/calejo/ # Configuration files
|
||||||
|
/var/backup/calejo/ # Backup files
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Access Points
|
||||||
|
|
||||||
|
After deployment, access the system at:
|
||||||
|
|
||||||
|
- **Dashboard**: `http://<server-ip>:8080/dashboard`
|
||||||
|
- **REST API**: `http://<server-ip>:8080`
|
||||||
|
- **Health Check**: `http://<server-ip>:8080/health`
|
||||||
|
- **Mock SCADA (OPC UA)**: `opc.tcp://<server-ip>:4840`
|
||||||
|
- **Mock SCADA (Modbus)**: `<server-ip>:502`
|
||||||
|
|
||||||
|
## 🔒 Security Considerations
|
||||||
|
|
||||||
|
### Default Credentials
|
||||||
|
|
||||||
|
The deployment includes security validation that warns about:
|
||||||
|
- Default database credentials
|
||||||
|
- Unsecured communication
|
||||||
|
- Open ports
|
||||||
|
|
||||||
|
### Recommended Security Practices
|
||||||
|
|
||||||
|
1. **Change default passwords** in configuration
|
||||||
|
2. **Enable authentication** in production
|
||||||
|
3. **Use SSL/TLS** for external communication
|
||||||
|
4. **Configure firewall** to restrict access
|
||||||
|
5. **Regular security updates**
|
||||||
|
|
||||||
|
## 📈 Monitoring and Maintenance
|
||||||
|
|
||||||
|
### Health Monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Regular health checks
|
||||||
|
/opt/calejo-control-adapter/scripts/health-check.sh
|
||||||
|
|
||||||
|
# Monitor logs
|
||||||
|
sudo tail -f /var/log/calejo/*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Strategy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Schedule regular backups (add to crontab)
|
||||||
|
0 2 * * * /opt/calejo-control-adapter/scripts/backup-full.sh
|
||||||
|
|
||||||
|
# Manual backup
|
||||||
|
/opt/calejo-control-adapter/scripts/backup-full.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Monitoring
|
||||||
|
|
||||||
|
The deployment includes:
|
||||||
|
- **Prometheus** metrics collection
|
||||||
|
- **Grafana** dashboards
|
||||||
|
- **Health monitoring** endpoints
|
||||||
|
- **Log aggregation**
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Application not starting**
|
||||||
|
```bash
|
||||||
|
# Check Docker status
|
||||||
|
sudo systemctl status docker
|
||||||
|
|
||||||
|
# Check application logs
|
||||||
|
sudo docker-compose logs app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Dashboard not accessible**
|
||||||
|
```bash
|
||||||
|
# Check if application is running
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
|
||||||
|
# Check firewall settings
|
||||||
|
sudo ufw status
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Mock servers not working**
|
||||||
|
```bash
|
||||||
|
# Check if required ports are available
|
||||||
|
sudo netstat -tulpn | grep -E ':(4840|502|8081)'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Files
|
||||||
|
|
||||||
|
- Application logs: `/var/log/calejo/`
|
||||||
|
- Docker logs: `sudo docker-compose logs`
|
||||||
|
- System logs: `/var/log/syslog`
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
For deployment issues:
|
||||||
|
|
||||||
|
1. Check this deployment guide
|
||||||
|
2. Run validation script: `./validate-deployment.sh`
|
||||||
|
3. Check logs in `/var/log/calejo/`
|
||||||
|
4. Review test results from `test-e2e-deployment.py`
|
||||||
|
|
||||||
|
## 🎯 Next Steps After Deployment
|
||||||
|
|
||||||
|
1. **Validate deployment** with `./validate-deployment.sh`
|
||||||
|
2. **Run end-to-end tests** with `python test-e2e-deployment.py`
|
||||||
|
3. **Configure monitoring** in Grafana
|
||||||
|
4. **Set up backups** with cron jobs
|
||||||
|
5. **Test integration** with real SCADA/optimization systems
|
||||||
|
6. **Train users** on dashboard usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Deployment Status**: ✅ Ready for Production
|
||||||
|
**Last Updated**: $(date)
|
||||||
|
**Version**: 1.0.0
|
||||||
|
|
@ -0,0 +1,388 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Calejo Control Adapter - On-Prem Deployment Script
|
||||||
|
# This script automates the deployment process for customer on-prem installations
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DEPLOYMENT_DIR="/opt/calejo-control-adapter"
|
||||||
|
LOG_DIR="/var/log/calejo"
|
||||||
|
CONFIG_DIR="/etc/calejo"
|
||||||
|
BACKUP_DIR="/var/backup/calejo"
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_status() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check if running as root
|
||||||
|
check_root() {
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
print_error "This script must be run as root for system-wide installation"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check prerequisites
|
||||||
|
check_prerequisites() {
|
||||||
|
print_status "Checking prerequisites..."
|
||||||
|
|
||||||
|
# Check Docker
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
print_error "Docker is not installed. Please install Docker first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Docker Compose
|
||||||
|
if ! command -v docker-compose &> /dev/null; then
|
||||||
|
print_error "Docker Compose is not installed. Please install Docker Compose first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check available disk space
|
||||||
|
local available_space=$(df / | awk 'NR==2 {print $4}')
|
||||||
|
if [[ $available_space -lt 1048576 ]]; then # Less than 1GB
|
||||||
|
print_warning "Low disk space available: ${available_space}KB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Prerequisites check passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create directories
|
||||||
|
create_directories() {
|
||||||
|
print_status "Creating directories..."
|
||||||
|
|
||||||
|
mkdir -p $DEPLOYMENT_DIR
|
||||||
|
mkdir -p $LOG_DIR
|
||||||
|
mkdir -p $CONFIG_DIR
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
mkdir -p $DEPLOYMENT_DIR/monitoring
|
||||||
|
mkdir -p $DEPLOYMENT_DIR/scripts
|
||||||
|
mkdir -p $DEPLOYMENT_DIR/database
|
||||||
|
|
||||||
|
print_success "Directories created"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to copy files
|
||||||
|
copy_files() {
|
||||||
|
print_status "Copying deployment files..."
|
||||||
|
|
||||||
|
# Copy main application files
|
||||||
|
cp -r ./* $DEPLOYMENT_DIR/
|
||||||
|
|
||||||
|
# Copy configuration files
|
||||||
|
cp config/settings.py $CONFIG_DIR/
|
||||||
|
cp docker-compose.yml $DEPLOYMENT_DIR/
|
||||||
|
cp docker-compose.test.yml $DEPLOYMENT_DIR/
|
||||||
|
|
||||||
|
# Copy scripts
|
||||||
|
cp scripts/* $DEPLOYMENT_DIR/scripts/
|
||||||
|
cp test-deployment.sh $DEPLOYMENT_DIR/
|
||||||
|
cp test_dashboard_local.py $DEPLOYMENT_DIR/
|
||||||
|
|
||||||
|
# Copy monitoring configuration
|
||||||
|
cp -r monitoring/* $DEPLOYMENT_DIR/monitoring/
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
chmod +x $DEPLOYMENT_DIR/scripts/*.sh
|
||||||
|
chmod +x $DEPLOYMENT_DIR/test-deployment.sh
|
||||||
|
|
||||||
|
print_success "Files copied to deployment directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create systemd service
|
||||||
|
create_systemd_service() {
|
||||||
|
print_status "Creating systemd service..."
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/calejo-control-adapter.service << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Calejo Control Adapter
|
||||||
|
Requires=docker.service
|
||||||
|
After=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
WorkingDirectory=$DEPLOYMENT_DIR
|
||||||
|
ExecStart=/usr/bin/docker-compose up -d
|
||||||
|
ExecStop=/usr/bin/docker-compose down
|
||||||
|
TimeoutStartSec=0
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
print_success "Systemd service created"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create backup script
|
||||||
|
create_backup_script() {
|
||||||
|
print_status "Creating backup script..."
|
||||||
|
|
||||||
|
cat > $DEPLOYMENT_DIR/scripts/backup-full.sh << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Full backup script for Calejo Control Adapter
|
||||||
|
|
||||||
|
BACKUP_DIR="/var/backup/calejo"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
BACKUP_FILE="calejo-backup-$TIMESTAMP.tar.gz"
|
||||||
|
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
echo "Stopping services..."
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
echo "Creating backup..."
|
||||||
|
tar -czf $BACKUP_DIR/$BACKUP_FILE \
|
||||||
|
--exclude=node_modules \
|
||||||
|
--exclude=__pycache__ \
|
||||||
|
--exclude=*.pyc \
|
||||||
|
.
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
echo "Starting services..."
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo "Backup created: $BACKUP_DIR/$BACKUP_FILE"
|
||||||
|
echo "Backup size: $(du -h $BACKUP_DIR/$BACKUP_FILE | cut -f1)"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x $DEPLOYMENT_DIR/scripts/backup-full.sh
|
||||||
|
print_success "Backup script created"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create restore script
|
||||||
|
create_restore_script() {
|
||||||
|
print_status "Creating restore script..."
|
||||||
|
|
||||||
|
cat > $DEPLOYMENT_DIR/scripts/restore-full.sh << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Full restore script for Calejo Control Adapter
|
||||||
|
|
||||||
|
BACKUP_DIR="/var/backup/calejo"
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Usage: $0 <backup-file>"
|
||||||
|
echo "Available backups:"
|
||||||
|
ls -la $BACKUP_DIR/calejo-backup-*.tar.gz 2>/dev/null || echo "No backups found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BACKUP_FILE="$1"
|
||||||
|
|
||||||
|
if [ ! -f "$BACKUP_FILE" ]; then
|
||||||
|
echo "Backup file not found: $BACKUP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
echo "Stopping services..."
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Restore backup
|
||||||
|
echo "Restoring from backup..."
|
||||||
|
tar -xzf "$BACKUP_FILE" -C .
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
echo "Starting services..."
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo "Restore completed from: $BACKUP_FILE"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x $DEPLOYMENT_DIR/scripts/restore-full.sh
|
||||||
|
print_success "Restore script created"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create health check script
|
||||||
|
create_health_check_script() {
|
||||||
|
print_status "Creating health check script..."
|
||||||
|
|
||||||
|
cat > $DEPLOYMENT_DIR/scripts/health-check.sh << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Health check script for Calejo Control Adapter
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
check_service() {
|
||||||
|
local service_name=$1
|
||||||
|
local port=$2
|
||||||
|
local endpoint=$3
|
||||||
|
|
||||||
|
if curl -s "http://localhost:$port$endpoint" > /dev/null; then
|
||||||
|
echo -e "${GREEN}✓${NC} $service_name is running on port $port"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} $service_name is not responding on port $port"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Running health checks..."
|
||||||
|
|
||||||
|
# Check main application
|
||||||
|
check_service "Main Application" 8080 "/health"
|
||||||
|
|
||||||
|
# Check dashboard
|
||||||
|
check_service "Dashboard" 8080 "/dashboard"
|
||||||
|
|
||||||
|
# Check API endpoints
|
||||||
|
check_service "REST API" 8080 "/api/v1/status"
|
||||||
|
|
||||||
|
# Check if containers are running
|
||||||
|
if docker-compose ps | grep -q "Up"; then
|
||||||
|
echo -e "${GREEN}✓${NC} All Docker containers are running"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Some Docker containers are not running"
|
||||||
|
docker-compose ps
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check disk space
|
||||||
|
echo ""
|
||||||
|
echo "System resources:"
|
||||||
|
df -h / | awk 'NR==2 {print "Disk usage: " $5 " (" $3 "/" $2 ")"}'
|
||||||
|
|
||||||
|
# Check memory
|
||||||
|
free -h | awk 'NR==2 {print "Memory usage: " $3 "/" $2}'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Health check completed"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x $DEPLOYMENT_DIR/scripts/health-check.sh
|
||||||
|
print_success "Health check script created"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to build and start services
|
||||||
|
build_and_start_services() {
|
||||||
|
print_status "Building and starting services..."
|
||||||
|
|
||||||
|
cd $DEPLOYMENT_DIR
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Wait for services to be ready
|
||||||
|
print_status "Waiting for services to start..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if curl -s http://localhost:8080/health > /dev/null 2>&1; then
|
||||||
|
print_success "Services started successfully"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo " Waiting... (attempt $i/30)"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if [ $i -eq 30 ]; then
|
||||||
|
print_error "Services failed to start within 60 seconds"
|
||||||
|
docker-compose logs
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to display deployment information
|
||||||
|
display_deployment_info() {
|
||||||
|
print_success "Deployment completed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo " DEPLOYMENT INFORMATION"
|
||||||
|
echo "=================================================="
|
||||||
|
echo ""
|
||||||
|
echo "📊 Access URLs:"
|
||||||
|
echo " Dashboard: http://$(hostname -I | awk '{print $1}'):8080/dashboard"
|
||||||
|
echo " REST API: http://$(hostname -I | awk '{print $1}'):8080"
|
||||||
|
echo " Health Check: http://$(hostname -I | awk '{print $1}'):8080/health"
|
||||||
|
echo ""
|
||||||
|
echo "🔧 Management Commands:"
|
||||||
|
echo " Start: systemctl start calejo-control-adapter"
|
||||||
|
echo " Stop: systemctl stop calejo-control-adapter"
|
||||||
|
echo " Status: systemctl status calejo-control-adapter"
|
||||||
|
echo " Health Check: $DEPLOYMENT_DIR/scripts/health-check.sh"
|
||||||
|
echo " Backup: $DEPLOYMENT_DIR/scripts/backup-full.sh"
|
||||||
|
echo ""
|
||||||
|
echo "📁 Important Directories:"
|
||||||
|
echo " Application: $DEPLOYMENT_DIR"
|
||||||
|
echo " Logs: $LOG_DIR"
|
||||||
|
echo " Configuration: $CONFIG_DIR"
|
||||||
|
echo " Backups: $BACKUP_DIR"
|
||||||
|
echo ""
|
||||||
|
echo "📚 Documentation:"
|
||||||
|
echo " Quick Start: $DEPLOYMENT_DIR/QUICKSTART.md"
|
||||||
|
echo " Dashboard: $DEPLOYMENT_DIR/DASHBOARD.md"
|
||||||
|
echo " Deployment: $DEPLOYMENT_DIR/DEPLOYMENT.md"
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main deployment function
|
||||||
|
main() {
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Calejo Control Adapter - On-Prem Deployment"
|
||||||
|
echo "=================================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
check_root
|
||||||
|
|
||||||
|
# Check prerequisites
|
||||||
|
check_prerequisites
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
create_directories
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
copy_files
|
||||||
|
|
||||||
|
# Create systemd service
|
||||||
|
create_systemd_service
|
||||||
|
|
||||||
|
# Create management scripts
|
||||||
|
create_backup_script
|
||||||
|
create_restore_script
|
||||||
|
create_health_check_script
|
||||||
|
|
||||||
|
# Build and start services
|
||||||
|
build_and_start_services
|
||||||
|
|
||||||
|
# Display deployment information
|
||||||
|
display_deployment_info
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_success "On-prem deployment completed!"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main "$@"
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Mock Optimization Server for Testing
|
||||||
|
Simulates an optimization system that provides setpoints and control strategies
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizationStrategy(Enum):
|
||||||
|
"""Optimization strategies"""
|
||||||
|
ENERGY_EFFICIENCY = "energy_efficiency"
|
||||||
|
COST_OPTIMIZATION = "cost_optimization"
|
||||||
|
PRODUCTION_MAXIMIZATION = "production_maximization"
|
||||||
|
QUALITY_OPTIMIZATION = "quality_optimization"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OptimizationResult:
|
||||||
|
"""Result of optimization calculation"""
|
||||||
|
strategy: OptimizationStrategy
|
||||||
|
setpoints: Dict[str, float]
|
||||||
|
cost_savings: float
|
||||||
|
energy_savings: float
|
||||||
|
production_increase: float
|
||||||
|
quality_improvement: float
|
||||||
|
confidence: float
|
||||||
|
timestamp: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class MockOptimizationServer:
|
||||||
|
"""Mock optimization server that simulates optimization calculations"""
|
||||||
|
|
||||||
|
def __init__(self, port: int = 8081):
|
||||||
|
self.port = port
|
||||||
|
self.running = False
|
||||||
|
self.current_strategy = OptimizationStrategy.ENERGY_EFFICIENCY
|
||||||
|
|
||||||
|
# Historical optimization results
|
||||||
|
self.optimization_history: List[OptimizationResult] = []
|
||||||
|
|
||||||
|
# Current process state (would come from SCADA)
|
||||||
|
self.current_state = {
|
||||||
|
'temperature': 25.0,
|
||||||
|
'pressure': 101.3,
|
||||||
|
'flow_rate': 100.0,
|
||||||
|
'level': 50.0,
|
||||||
|
'energy_consumption': 150.0,
|
||||||
|
'production_rate': 85.0,
|
||||||
|
'quality_metric': 92.0,
|
||||||
|
'operating_cost': 1250.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Market and external factors
|
||||||
|
self.market_data = {
|
||||||
|
'energy_price': 0.12, # $/kWh
|
||||||
|
'raw_material_cost': 45.0, # $/ton
|
||||||
|
'product_price': 150.0, # $/unit
|
||||||
|
'demand_factor': 0.95
|
||||||
|
}
|
||||||
|
|
||||||
|
def calculate_optimization(self, strategy: OptimizationStrategy) -> OptimizationResult:
|
||||||
|
"""Calculate optimization based on current state and strategy"""
|
||||||
|
|
||||||
|
# Simulate optimization calculation
|
||||||
|
base_setpoints = {
|
||||||
|
'temperature_setpoint': 75.0,
|
||||||
|
'pressure_setpoint': 105.0,
|
||||||
|
'flow_setpoint': 120.0,
|
||||||
|
'level_setpoint': 60.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Adjust setpoints based on strategy
|
||||||
|
if strategy == OptimizationStrategy.ENERGY_EFFICIENCY:
|
||||||
|
setpoints = {
|
||||||
|
'temperature_setpoint': base_setpoints['temperature_setpoint'] - 2.0,
|
||||||
|
'pressure_setpoint': base_setpoints['pressure_setpoint'] - 1.0,
|
||||||
|
'flow_setpoint': base_setpoints['flow_setpoint'] - 5.0,
|
||||||
|
'level_setpoint': base_setpoints['level_setpoint']
|
||||||
|
}
|
||||||
|
cost_savings = random.uniform(5.0, 15.0)
|
||||||
|
energy_savings = random.uniform(8.0, 20.0)
|
||||||
|
production_increase = random.uniform(-2.0, 2.0)
|
||||||
|
quality_improvement = random.uniform(-1.0, 1.0)
|
||||||
|
|
||||||
|
elif strategy == OptimizationStrategy.COST_OPTIMIZATION:
|
||||||
|
setpoints = {
|
||||||
|
'temperature_setpoint': base_setpoints['temperature_setpoint'] - 1.0,
|
||||||
|
'pressure_setpoint': base_setpoints['pressure_setpoint'] - 0.5,
|
||||||
|
'flow_setpoint': base_setpoints['flow_setpoint'] - 3.0,
|
||||||
|
'level_setpoint': base_setpoints['level_setpoint'] + 5.0
|
||||||
|
}
|
||||||
|
cost_savings = random.uniform(10.0, 25.0)
|
||||||
|
energy_savings = random.uniform(5.0, 12.0)
|
||||||
|
production_increase = random.uniform(1.0, 5.0)
|
||||||
|
quality_improvement = random.uniform(0.0, 2.0)
|
||||||
|
|
||||||
|
elif strategy == OptimizationStrategy.PRODUCTION_MAXIMIZATION:
|
||||||
|
setpoints = {
|
||||||
|
'temperature_setpoint': base_setpoints['temperature_setpoint'] + 3.0,
|
||||||
|
'pressure_setpoint': base_setpoints['pressure_setpoint'] + 2.0,
|
||||||
|
'flow_setpoint': base_setpoints['flow_setpoint'] + 10.0,
|
||||||
|
'level_setpoint': base_setpoints['level_setpoint'] - 5.0
|
||||||
|
}
|
||||||
|
cost_savings = random.uniform(-5.0, 5.0)
|
||||||
|
energy_savings = random.uniform(-10.0, 0.0)
|
||||||
|
production_increase = random.uniform(8.0, 15.0)
|
||||||
|
quality_improvement = random.uniform(-3.0, 0.0)
|
||||||
|
|
||||||
|
else: # QUALITY_OPTIMIZATION
|
||||||
|
setpoints = {
|
||||||
|
'temperature_setpoint': base_setpoints['temperature_setpoint'] + 1.0,
|
||||||
|
'pressure_setpoint': base_setpoints['pressure_setpoint'] + 0.5,
|
||||||
|
'flow_setpoint': base_setpoints['flow_setpoint'] - 2.0,
|
||||||
|
'level_setpoint': base_setpoints['level_setpoint'] + 2.0
|
||||||
|
}
|
||||||
|
cost_savings = random.uniform(2.0, 8.0)
|
||||||
|
energy_savings = random.uniform(3.0, 8.0)
|
||||||
|
production_increase = random.uniform(-1.0, 1.0)
|
||||||
|
quality_improvement = random.uniform(5.0, 12.0)
|
||||||
|
|
||||||
|
# Add some randomness to simulate real optimization
|
||||||
|
for key in setpoints:
|
||||||
|
setpoints[key] += random.uniform(-1.0, 1.0)
|
||||||
|
|
||||||
|
# Calculate confidence based on strategy and current conditions
|
||||||
|
confidence = random.uniform(0.7, 0.95)
|
||||||
|
|
||||||
|
result = OptimizationResult(
|
||||||
|
strategy=strategy,
|
||||||
|
setpoints=setpoints,
|
||||||
|
cost_savings=cost_savings,
|
||||||
|
energy_savings=energy_savings,
|
||||||
|
production_increase=production_increase,
|
||||||
|
quality_improvement=quality_improvement,
|
||||||
|
confidence=confidence,
|
||||||
|
timestamp=datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_optimal_strategy(self) -> OptimizationStrategy:
|
||||||
|
"""Determine the best strategy based on current conditions"""
|
||||||
|
|
||||||
|
# Simple heuristic based on current state and market conditions
|
||||||
|
if self.market_data['energy_price'] > 0.15:
|
||||||
|
return OptimizationStrategy.ENERGY_EFFICIENCY
|
||||||
|
elif self.market_data['demand_factor'] > 1.1:
|
||||||
|
return OptimizationStrategy.PRODUCTION_MAXIMIZATION
|
||||||
|
elif self.current_state['quality_metric'] < 90.0:
|
||||||
|
return OptimizationStrategy.QUALITY_OPTIMIZATION
|
||||||
|
else:
|
||||||
|
return OptimizationStrategy.COST_OPTIMIZATION
|
||||||
|
|
||||||
|
async def update_market_data(self):
|
||||||
|
"""Simulate changing market conditions"""
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
# Simulate market fluctuations
|
||||||
|
self.market_data['energy_price'] += random.uniform(-0.01, 0.01)
|
||||||
|
self.market_data['energy_price'] = max(0.08, min(0.20, self.market_data['energy_price']))
|
||||||
|
|
||||||
|
self.market_data['raw_material_cost'] += random.uniform(-1.0, 1.0)
|
||||||
|
self.market_data['raw_material_cost'] = max(40.0, min(60.0, self.market_data['raw_material_cost']))
|
||||||
|
|
||||||
|
self.market_data['demand_factor'] += random.uniform(-0.05, 0.05)
|
||||||
|
self.market_data['demand_factor'] = max(0.8, min(1.3, self.market_data['demand_factor']))
|
||||||
|
|
||||||
|
await asyncio.sleep(30) # Update every 30 seconds
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating market data: {e}")
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
async def run_optimization_cycle(self):
|
||||||
|
"""Run optimization cycles periodically"""
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
# Get optimal strategy
|
||||||
|
strategy = self.get_optimal_strategy()
|
||||||
|
self.current_strategy = strategy
|
||||||
|
|
||||||
|
# Calculate optimization
|
||||||
|
result = self.calculate_optimization(strategy)
|
||||||
|
|
||||||
|
# Store in history
|
||||||
|
self.optimization_history.append(result)
|
||||||
|
|
||||||
|
# Keep only last 100 optimizations
|
||||||
|
if len(self.optimization_history) > 100:
|
||||||
|
self.optimization_history = self.optimization_history[-100:]
|
||||||
|
|
||||||
|
logger.info(f"Optimization completed: {strategy.value} - Confidence: {result.confidence:.2f}")
|
||||||
|
|
||||||
|
await asyncio.sleep(60) # Run optimization every minute
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in optimization cycle: {e}")
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get server status"""
|
||||||
|
latest_result = self.optimization_history[-1] if self.optimization_history else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'running': self.running,
|
||||||
|
'current_strategy': self.current_strategy.value if self.current_strategy else None,
|
||||||
|
'market_data': self.market_data,
|
||||||
|
'optimization_count': len(self.optimization_history),
|
||||||
|
'latest_optimization': {
|
||||||
|
'strategy': latest_result.strategy.value if latest_result else None,
|
||||||
|
'setpoints': latest_result.setpoints if latest_result else {},
|
||||||
|
'confidence': latest_result.confidence if latest_result else 0.0,
|
||||||
|
'timestamp': latest_result.timestamp.isoformat() if latest_result else None
|
||||||
|
} if latest_result else None
|
||||||
|
}
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start the mock optimization server"""
|
||||||
|
if self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
# Start background tasks
|
||||||
|
self.market_task = asyncio.create_task(self.update_market_data())
|
||||||
|
self.optimization_task = asyncio.create_task(self.run_optimization_cycle())
|
||||||
|
|
||||||
|
logger.info(f"Mock optimization server started")
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop the mock optimization server"""
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
if hasattr(self, 'market_task'):
|
||||||
|
self.market_task.cancel()
|
||||||
|
if hasattr(self, 'optimization_task'):
|
||||||
|
self.optimization_task.cancel()
|
||||||
|
|
||||||
|
logger.info("Mock optimization server stopped")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main function to run the mock optimization server"""
|
||||||
|
server = MockOptimizationServer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await server.start()
|
||||||
|
|
||||||
|
# Keep server running
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down mock optimization server...")
|
||||||
|
await server.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Mock SCADA Server for Testing
|
||||||
|
Simulates a real SCADA system with OPC UA and Modbus interfaces
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
# OPC UA imports
|
||||||
|
try:
|
||||||
|
from asyncua import Server, Node
|
||||||
|
OPCUA_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
OPCUA_AVAILABLE = False
|
||||||
|
print("Warning: asyncua not available. Install with: pip install asyncua")
|
||||||
|
|
||||||
|
# Modbus imports
|
||||||
|
try:
|
||||||
|
from pymodbus.server import StartTcpServer
|
||||||
|
from pymodbus.datastore import ModbusSequentialDataBlock
|
||||||
|
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
|
||||||
|
MODBUS_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
MODBUS_AVAILABLE = False
|
||||||
|
print("Warning: pymodbus not available. Install with: pip install pymodbus")
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MockSCADAServer:
|
||||||
|
"""Mock SCADA server that simulates industrial control systems"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.opcua_server = None
|
||||||
|
self.modbus_server = None
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
# Simulated process data
|
||||||
|
self.process_data = {
|
||||||
|
'temperature': 25.0,
|
||||||
|
'pressure': 101.3,
|
||||||
|
'flow_rate': 100.0,
|
||||||
|
'level': 50.0,
|
||||||
|
'valve_position': 75.0,
|
||||||
|
'pump_status': True,
|
||||||
|
'alarm_status': False,
|
||||||
|
'setpoint': 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Historical data for trends
|
||||||
|
self.historical_data = {
|
||||||
|
'temperature': [],
|
||||||
|
'pressure': [],
|
||||||
|
'flow_rate': []
|
||||||
|
}
|
||||||
|
|
||||||
|
async def start_opcua_server(self, endpoint: str = "opc.tcp://0.0.0.0:4840"):
|
||||||
|
"""Start OPC UA server"""
|
||||||
|
if not OPCUA_AVAILABLE:
|
||||||
|
logger.warning("OPC UA not available, skipping OPC UA server")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.opcua_server = Server()
|
||||||
|
await self.opcua_server.init()
|
||||||
|
|
||||||
|
self.opcua_server.set_endpoint(endpoint)
|
||||||
|
self.opcua_server.set_server_name("Mock SCADA Server")
|
||||||
|
|
||||||
|
# Setup namespace
|
||||||
|
uri = "http://mock-scada.org"
|
||||||
|
idx = await self.opcua_server.register_namespace(uri)
|
||||||
|
|
||||||
|
# Create object node
|
||||||
|
objects = self.opcua_server.get_objects_node()
|
||||||
|
scada_object = await objects.add_object(idx, "SCADA_System")
|
||||||
|
|
||||||
|
# Add process variables
|
||||||
|
self.opcua_nodes = {}
|
||||||
|
for name, value in self.process_data.items():
|
||||||
|
if isinstance(value, bool):
|
||||||
|
node = await scada_object.add_variable(idx, name, value)
|
||||||
|
elif isinstance(value, (int, float)):
|
||||||
|
node = await scada_object.add_variable(idx, name, float(value))
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
await node.set_writable()
|
||||||
|
self.opcua_nodes[name] = node
|
||||||
|
|
||||||
|
await self.opcua_server.start()
|
||||||
|
logger.info(f"Mock OPC UA server started at {endpoint}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start OPC UA server: {e}")
|
||||||
|
|
||||||
|
def start_modbus_server(self, port: int = 502):
|
||||||
|
"""Start Modbus TCP server"""
|
||||||
|
if not MODBUS_AVAILABLE:
|
||||||
|
logger.warning("Modbus not available, skipping Modbus server")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create data blocks
|
||||||
|
store = ModbusSlaveContext(
|
||||||
|
di=ModbusSequentialDataBlock(0, [0]*100), # Discrete Inputs
|
||||||
|
co=ModbusSequentialDataBlock(0, [0]*100), # Coils
|
||||||
|
hr=ModbusSequentialDataBlock(0, [0]*100), # Holding Registers
|
||||||
|
ir=ModbusSequentialDataBlock(0, [0]*100) # Input Registers
|
||||||
|
)
|
||||||
|
|
||||||
|
context = ModbusServerContext(slaves=store, single=True)
|
||||||
|
|
||||||
|
# Start server in background thread
|
||||||
|
import threading
|
||||||
|
def run_modbus_server():
|
||||||
|
StartTcpServer(context=context, address=("0.0.0.0", port))
|
||||||
|
|
||||||
|
modbus_thread = threading.Thread(target=run_modbus_server, daemon=True)
|
||||||
|
modbus_thread.start()
|
||||||
|
|
||||||
|
self.modbus_server = modbus_thread
|
||||||
|
logger.info(f"Mock Modbus server started on port {port}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start Modbus server: {e}")
|
||||||
|
|
||||||
|
async def update_process_data(self):
|
||||||
|
"""Update simulated process data"""
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
# Simulate realistic process variations
|
||||||
|
self.process_data['temperature'] += random.uniform(-0.5, 0.5)
|
||||||
|
self.process_data['temperature'] = max(20.0, min(80.0, self.process_data['temperature']))
|
||||||
|
|
||||||
|
self.process_data['pressure'] += random.uniform(-0.1, 0.1)
|
||||||
|
self.process_data['pressure'] = max(95.0, min(110.0, self.process_data['pressure']))
|
||||||
|
|
||||||
|
self.process_data['flow_rate'] += random.uniform(-2.0, 2.0)
|
||||||
|
self.process_data['flow_rate'] = max(0.0, min(200.0, self.process_data['flow_rate']))
|
||||||
|
|
||||||
|
self.process_data['level'] += random.uniform(-1.0, 1.0)
|
||||||
|
self.process_data['level'] = max(0.0, min(100.0, self.process_data['level']))
|
||||||
|
|
||||||
|
# Simulate valve and pump behavior
|
||||||
|
if self.process_data['flow_rate'] > 150:
|
||||||
|
self.process_data['valve_position'] = max(0, self.process_data['valve_position'] - 1)
|
||||||
|
elif self.process_data['flow_rate'] < 50:
|
||||||
|
self.process_data['valve_position'] = min(100, self.process_data['valve_position'] + 1)
|
||||||
|
|
||||||
|
# Simulate alarms
|
||||||
|
self.process_data['alarm_status'] = (
|
||||||
|
self.process_data['temperature'] > 75.0 or
|
||||||
|
self.process_data['pressure'] > 108.0 or
|
||||||
|
self.process_data['level'] > 95.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update OPC UA nodes if available
|
||||||
|
if self.opcua_server and self.opcua_nodes:
|
||||||
|
for name, node in self.opcua_nodes.items():
|
||||||
|
await node.write_value(self.process_data[name])
|
||||||
|
|
||||||
|
# Store historical data
|
||||||
|
timestamp = datetime.now()
|
||||||
|
self.historical_data['temperature'].append({
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'value': self.process_data['temperature']
|
||||||
|
})
|
||||||
|
self.historical_data['pressure'].append({
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'value': self.process_data['pressure']
|
||||||
|
})
|
||||||
|
self.historical_data['flow_rate'].append({
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'value': self.process_data['flow_rate']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Keep only last 1000 points
|
||||||
|
for key in self.historical_data:
|
||||||
|
if len(self.historical_data[key]) > 1000:
|
||||||
|
self.historical_data[key] = self.historical_data[key][-1000:]
|
||||||
|
|
||||||
|
await asyncio.sleep(1) # Update every second
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating process data: {e}")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get server status"""
|
||||||
|
return {
|
||||||
|
'running': self.running,
|
||||||
|
'opcua_available': OPCUA_AVAILABLE and self.opcua_server is not None,
|
||||||
|
'modbus_available': MODBUS_AVAILABLE and self.modbus_server is not None,
|
||||||
|
'process_data': self.process_data,
|
||||||
|
'data_points': sum(len(data) for data in self.historical_data.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start the mock SCADA server"""
|
||||||
|
if self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
# Start servers
|
||||||
|
await self.start_opcua_server()
|
||||||
|
self.start_modbus_server()
|
||||||
|
|
||||||
|
# Start data update loop
|
||||||
|
self.update_task = asyncio.create_task(self.update_process_data())
|
||||||
|
|
||||||
|
logger.info("Mock SCADA server started")
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop the mock SCADA server"""
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
if hasattr(self, 'update_task'):
|
||||||
|
self.update_task.cancel()
|
||||||
|
try:
|
||||||
|
await self.update_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.opcua_server:
|
||||||
|
await self.opcua_server.stop()
|
||||||
|
|
||||||
|
logger.info("Mock SCADA server stopped")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main function to run the mock SCADA server"""
|
||||||
|
server = MockSCADAServer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await server.start()
|
||||||
|
|
||||||
|
# Keep server running
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down mock SCADA server...")
|
||||||
|
await server.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -0,0 +1,394 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
End-to-End Deployment Testing Script
|
||||||
|
Tests the complete Calejo Control Adapter system with mock SCADA and optimization
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class E2ETestRunner:
|
||||||
|
"""End-to-end test runner for Calejo Control Adapter"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.base_url = "http://localhost:8080"
|
||||||
|
self.mock_scada_process = None
|
||||||
|
self.mock_optimization_process = None
|
||||||
|
self.main_app_process = None
|
||||||
|
|
||||||
|
# Test results
|
||||||
|
self.test_results = []
|
||||||
|
|
||||||
|
def start_mock_servers(self):
|
||||||
|
"""Start mock SCADA and optimization servers"""
|
||||||
|
logger.info("Starting mock servers...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Start mock SCADA server
|
||||||
|
self.mock_scada_process = subprocess.Popen(
|
||||||
|
[sys.executable, "mock-scada-server.py"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start mock optimization server
|
||||||
|
self.mock_optimization_process = subprocess.Popen(
|
||||||
|
[sys.executable, "mock-optimization-server.py"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for servers to start
|
||||||
|
time.sleep(5)
|
||||||
|
logger.info("Mock servers started")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start mock servers: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_main_application(self):
|
||||||
|
"""Start the main Calejo Control Adapter application"""
|
||||||
|
logger.info("Starting main application...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.main_app_process = subprocess.Popen(
|
||||||
|
[sys.executable, "src/main.py"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for application to start
|
||||||
|
for i in range(30):
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.base_url}/health", timeout=2)
|
||||||
|
if response.status_code == 200:
|
||||||
|
logger.info("Main application started")
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
if i % 5 == 0:
|
||||||
|
logger.info(f"Waiting for application to start... ({i*2}s)")
|
||||||
|
|
||||||
|
logger.error("Main application failed to start within 60 seconds")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to start main application: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_servers(self):
|
||||||
|
"""Stop all running servers"""
|
||||||
|
logger.info("Stopping servers...")
|
||||||
|
|
||||||
|
if self.main_app_process:
|
||||||
|
self.main_app_process.terminate()
|
||||||
|
self.main_app_process.wait(timeout=10)
|
||||||
|
|
||||||
|
if self.mock_scada_process:
|
||||||
|
self.mock_scada_process.terminate()
|
||||||
|
self.mock_scada_process.wait(timeout=10)
|
||||||
|
|
||||||
|
if self.mock_optimization_process:
|
||||||
|
self.mock_optimization_process.terminate()
|
||||||
|
self.mock_optimization_process.wait(timeout=10)
|
||||||
|
|
||||||
|
logger.info("All servers stopped")
|
||||||
|
|
||||||
|
def test_health_endpoint(self) -> bool:
|
||||||
|
"""Test health endpoint"""
|
||||||
|
logger.info("Testing health endpoint...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.base_url}/health")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
logger.info(f"Health status: {data.get('status', 'unknown')}")
|
||||||
|
self.test_results.append(("Health Endpoint", True, "Health check successful"))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Health endpoint returned {response.status_code}")
|
||||||
|
self.test_results.append(("Health Endpoint", False, f"HTTP {response.status_code}"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Health endpoint test failed: {e}")
|
||||||
|
self.test_results.append(("Health Endpoint", False, str(e)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_dashboard_access(self) -> bool:
|
||||||
|
"""Test dashboard access"""
|
||||||
|
logger.info("Testing dashboard access...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.base_url}/dashboard")
|
||||||
|
if response.status_code == 200:
|
||||||
|
if "Calejo Control Adapter Dashboard" in response.text:
|
||||||
|
logger.info("Dashboard HTML loaded successfully")
|
||||||
|
self.test_results.append(("Dashboard Access", True, "Dashboard loaded"))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Dashboard HTML content incorrect")
|
||||||
|
self.test_results.append(("Dashboard Access", False, "Incorrect content"))
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.error(f"Dashboard returned {response.status_code}")
|
||||||
|
self.test_results.append(("Dashboard Access", False, f"HTTP {response.status_code}"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Dashboard access test failed: {e}")
|
||||||
|
self.test_results.append(("Dashboard Access", False, str(e)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_dashboard_api(self) -> bool:
|
||||||
|
"""Test dashboard API endpoints"""
|
||||||
|
logger.info("Testing dashboard API...")
|
||||||
|
|
||||||
|
endpoints = [
|
||||||
|
("/api/v1/dashboard/status", "Status API"),
|
||||||
|
("/api/v1/dashboard/config", "Config API"),
|
||||||
|
("/api/v1/dashboard/logs", "Logs API"),
|
||||||
|
("/api/v1/dashboard/actions", "Actions API")
|
||||||
|
]
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
|
||||||
|
for endpoint, name in endpoints:
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.base_url}{endpoint}")
|
||||||
|
if response.status_code == 200:
|
||||||
|
logger.info(f"{name}: OK")
|
||||||
|
self.test_results.append((f"{name}", True, "API accessible"))
|
||||||
|
else:
|
||||||
|
logger.error(f"{name}: HTTP {response.status_code}")
|
||||||
|
self.test_results.append((f"{name}", False, f"HTTP {response.status_code}"))
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"{name} test failed: {e}")
|
||||||
|
self.test_results.append((f"{name}", False, str(e)))
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
return all_passed
|
||||||
|
|
||||||
|
def test_configuration_management(self) -> bool:
|
||||||
|
"""Test configuration management"""
|
||||||
|
logger.info("Testing configuration management...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get current configuration
|
||||||
|
response = requests.get(f"{self.base_url}/api/v1/dashboard/config")
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error("Failed to get configuration")
|
||||||
|
self.test_results.append(("Configuration Management", False, "Get config failed"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
config = response.json()
|
||||||
|
|
||||||
|
# Test configuration update
|
||||||
|
test_config = {
|
||||||
|
"database": {
|
||||||
|
"host": "test-host",
|
||||||
|
"port": 5432,
|
||||||
|
"name": "test-db",
|
||||||
|
"user": "test-user",
|
||||||
|
"password": "test-pass"
|
||||||
|
},
|
||||||
|
"opcua": {
|
||||||
|
"enabled": True,
|
||||||
|
"port": 4840
|
||||||
|
},
|
||||||
|
"modbus": {
|
||||||
|
"enabled": True,
|
||||||
|
"port": 502
|
||||||
|
},
|
||||||
|
"rest_api": {
|
||||||
|
"enabled": True,
|
||||||
|
"port": 8080
|
||||||
|
},
|
||||||
|
"monitoring": {
|
||||||
|
"enabled": True,
|
||||||
|
"port": 9090
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"enable_auth": False,
|
||||||
|
"enable_ssl": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.base_url}/api/v1/dashboard/config",
|
||||||
|
json=test_config
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
if result.get("success", False):
|
||||||
|
logger.info("Configuration update successful")
|
||||||
|
self.test_results.append(("Configuration Management", True, "Config update successful"))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Configuration update failed: {result.get('error', 'Unknown error')}")
|
||||||
|
self.test_results.append(("Configuration Management", False, result.get('error', 'Unknown error')))
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.error(f"Configuration update returned {response.status_code}")
|
||||||
|
self.test_results.append(("Configuration Management", False, f"HTTP {response.status_code}"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Configuration management test failed: {e}")
|
||||||
|
self.test_results.append(("Configuration Management", False, str(e)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_system_actions(self) -> bool:
|
||||||
|
"""Test system actions"""
|
||||||
|
logger.info("Testing system actions...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test health check action
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.base_url}/api/v1/dashboard/actions",
|
||||||
|
json={"action": "health_check"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
if result.get("success", False):
|
||||||
|
logger.info("Health check action successful")
|
||||||
|
self.test_results.append(("System Actions", True, "Health check successful"))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Health check failed: {result.get('error', 'Unknown error')}")
|
||||||
|
self.test_results.append(("System Actions", False, result.get('error', 'Unknown error')))
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.error(f"Health check action returned {response.status_code}")
|
||||||
|
self.test_results.append(("System Actions", False, f"HTTP {response.status_code}"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"System actions test failed: {e}")
|
||||||
|
self.test_results.append(("System Actions", False, str(e)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_integration_with_mock_servers(self) -> bool:
|
||||||
|
"""Test integration with mock SCADA and optimization servers"""
|
||||||
|
logger.info("Testing integration with mock servers...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test if mock SCADA is responding (OPC UA would require specific client)
|
||||||
|
# For now, just check if processes are running
|
||||||
|
if (self.mock_scada_process and self.mock_scada_process.poll() is None and
|
||||||
|
self.mock_optimization_process and self.mock_optimization_process.poll() is None):
|
||||||
|
logger.info("Mock servers are running")
|
||||||
|
self.test_results.append(("Mock Server Integration", True, "Mock servers running"))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error("Mock servers are not running")
|
||||||
|
self.test_results.append(("Mock Server Integration", False, "Mock servers not running"))
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Integration test failed: {e}")
|
||||||
|
self.test_results.append(("Mock Server Integration", False, str(e)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_all_tests(self) -> bool:
|
||||||
|
"""Run all end-to-end tests"""
|
||||||
|
logger.info("Starting end-to-end deployment tests...")
|
||||||
|
|
||||||
|
# Start servers
|
||||||
|
if not self.start_mock_servers():
|
||||||
|
logger.error("Failed to start mock servers")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.start_main_application():
|
||||||
|
logger.error("Failed to start main application")
|
||||||
|
self.stop_servers()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
tests = [
|
||||||
|
self.test_health_endpoint,
|
||||||
|
self.test_dashboard_access,
|
||||||
|
self.test_dashboard_api,
|
||||||
|
self.test_configuration_management,
|
||||||
|
self.test_system_actions,
|
||||||
|
self.test_integration_with_mock_servers
|
||||||
|
]
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
for test_func in tests:
|
||||||
|
if not test_func():
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Stop servers
|
||||||
|
self.stop_servers()
|
||||||
|
|
||||||
|
# Print results
|
||||||
|
self.print_test_results()
|
||||||
|
|
||||||
|
return all_passed
|
||||||
|
|
||||||
|
def print_test_results(self):
|
||||||
|
"""Print test results summary"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("END-TO-END DEPLOYMENT TEST RESULTS")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
total = len(self.test_results)
|
||||||
|
|
||||||
|
for test_name, success, message in self.test_results:
|
||||||
|
status = "✅ PASS" if success else "❌ FAIL"
|
||||||
|
print(f"{status} {test_name}: {message}")
|
||||||
|
if success:
|
||||||
|
passed += 1
|
||||||
|
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(f"SUMMARY: {passed}/{total} tests passed")
|
||||||
|
|
||||||
|
if passed == total:
|
||||||
|
print("🎉 SUCCESS: All end-to-end tests passed!")
|
||||||
|
print("The deployment is ready for production use.")
|
||||||
|
else:
|
||||||
|
print("❌ Some tests failed. Please check the deployment.")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
print("🚀 Calejo Control Adapter - End-to-End Deployment Test")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# Check if required files exist
|
||||||
|
required_files = ["mock-scada-server.py", "mock-optimization-server.py", "src/main.py"]
|
||||||
|
for file in required_files:
|
||||||
|
if not Path(file).exists():
|
||||||
|
print(f"❌ Required file not found: {file}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test_runner = E2ETestRunner()
|
||||||
|
success = test_runner.run_all_tests()
|
||||||
|
|
||||||
|
return 0 if success else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -0,0 +1,353 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Calejo Control Adapter - Deployment Validation Script
|
||||||
|
# Validates that the deployment is healthy and ready for production
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
BASE_URL="http://localhost:8080"
|
||||||
|
DEPLOYMENT_DIR="/opt/calejo-control-adapter"
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_status() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check service health
|
||||||
|
check_service_health() {
|
||||||
|
local service_name=$1
|
||||||
|
local port=$2
|
||||||
|
local endpoint=$3
|
||||||
|
|
||||||
|
if curl -s -f "http://localhost:$port$endpoint" > /dev/null; then
|
||||||
|
print_success "$service_name is healthy (port $port)"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "$service_name is not responding (port $port)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check container status
|
||||||
|
check_container_status() {
|
||||||
|
print_status "Checking Docker container status..."
|
||||||
|
|
||||||
|
if command -v docker-compose > /dev/null && [ -f "docker-compose.yml" ]; then
|
||||||
|
cd $DEPLOYMENT_DIR
|
||||||
|
|
||||||
|
if docker-compose ps | grep -q "Up"; then
|
||||||
|
print_success "All Docker containers are running"
|
||||||
|
docker-compose ps --format "table {{.Service}}\t{{.State}}\t{{.Ports}}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Some Docker containers are not running"
|
||||||
|
docker-compose ps
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "Docker Compose not available or docker-compose.yml not found"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check system resources
|
||||||
|
check_system_resources() {
|
||||||
|
print_status "Checking system resources..."
|
||||||
|
|
||||||
|
# Check disk space
|
||||||
|
local disk_usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||||
|
if [ $disk_usage -gt 90 ]; then
|
||||||
|
print_error "Disk usage is high: ${disk_usage}%"
|
||||||
|
elif [ $disk_usage -gt 80 ]; then
|
||||||
|
print_warning "Disk usage is moderate: ${disk_usage}%"
|
||||||
|
else
|
||||||
|
print_success "Disk usage is normal: ${disk_usage}%"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check memory
|
||||||
|
local mem_info=$(free -h)
|
||||||
|
print_status "Memory usage:"
|
||||||
|
echo "$mem_info" | head -2
|
||||||
|
|
||||||
|
# Check CPU load
|
||||||
|
local load_avg=$(cat /proc/loadavg | awk '{print $1}')
|
||||||
|
local cpu_cores=$(nproc)
|
||||||
|
local load_percent=$(echo "scale=0; $load_avg * 100 / $cpu_cores" | bc)
|
||||||
|
|
||||||
|
if [ $load_percent -gt 90 ]; then
|
||||||
|
print_error "CPU load is high: ${load_avg} (${load_percent}% of capacity)"
|
||||||
|
elif [ $load_percent -gt 70 ]; then
|
||||||
|
print_warning "CPU load is moderate: ${load_avg} (${load_percent}% of capacity)"
|
||||||
|
else
|
||||||
|
print_success "CPU load is normal: ${load_avg} (${load_percent}% of capacity)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check application endpoints
|
||||||
|
check_application_endpoints() {
|
||||||
|
print_status "Checking application endpoints..."
|
||||||
|
|
||||||
|
endpoints=(
|
||||||
|
"/health"
|
||||||
|
"/dashboard"
|
||||||
|
"/api/v1/status"
|
||||||
|
"/api/v1/dashboard/status"
|
||||||
|
"/api/v1/dashboard/config"
|
||||||
|
"/api/v1/dashboard/logs"
|
||||||
|
"/api/v1/dashboard/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
all_healthy=true
|
||||||
|
|
||||||
|
for endpoint in "${endpoints[@]}"; do
|
||||||
|
if curl -s -f "$BASE_URL$endpoint" > /dev/null; then
|
||||||
|
print_success "Endpoint $endpoint is accessible"
|
||||||
|
else
|
||||||
|
print_error "Endpoint $endpoint is not accessible"
|
||||||
|
all_healthy=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if $all_healthy; then
|
||||||
|
print_success "All application endpoints are accessible"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Some application endpoints are not accessible"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check configuration
|
||||||
|
check_configuration() {
|
||||||
|
print_status "Checking configuration..."
|
||||||
|
|
||||||
|
# Check if configuration files exist
|
||||||
|
config_files=(
|
||||||
|
"$DEPLOYMENT_DIR/config/settings.py"
|
||||||
|
"$DEPLOYMENT_DIR/docker-compose.yml"
|
||||||
|
"$CONFIG_DIR/settings.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
for config_file in "${config_files[@]}"; do
|
||||||
|
if [ -f "$config_file" ]; then
|
||||||
|
print_success "Configuration file exists: $config_file"
|
||||||
|
else
|
||||||
|
print_warning "Configuration file missing: $config_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if configuration is valid
|
||||||
|
if curl -s "$BASE_URL/api/v1/dashboard/config" | grep -q '"success":true'; then
|
||||||
|
print_success "Configuration is valid and accessible"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Configuration validation failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check logs
|
||||||
|
check_logs() {
|
||||||
|
print_status "Checking logs..."
|
||||||
|
|
||||||
|
log_dirs=(
|
||||||
|
"/var/log/calejo"
|
||||||
|
"$DEPLOYMENT_DIR/logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
for log_dir in "${log_dirs[@]}"; do
|
||||||
|
if [ -d "$log_dir" ]; then
|
||||||
|
local log_count=$(find "$log_dir" -name "*.log" -type f | wc -l)
|
||||||
|
if [ $log_count -gt 0 ]; then
|
||||||
|
print_success "Log directory contains $log_count log files: $log_dir"
|
||||||
|
|
||||||
|
# Check for recent errors
|
||||||
|
local error_count=$(find "$log_dir" -name "*.log" -type f -exec grep -l -i "error\|exception\|fail" {} \; | wc -l)
|
||||||
|
if [ $error_count -gt 0 ]; then
|
||||||
|
print_warning "Found $error_count log files with errors"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "Log directory exists but contains no log files: $log_dir"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "Log directory does not exist: $log_dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check security
|
||||||
|
check_security() {
|
||||||
|
print_status "Checking security configuration..."
|
||||||
|
|
||||||
|
# Check for default credentials warning
|
||||||
|
if curl -s "$BASE_URL/api/v1/dashboard/config" | grep -q '"security_warning":true'; then
|
||||||
|
print_warning "Security warning: Default credentials detected"
|
||||||
|
else
|
||||||
|
print_success "No security warnings detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if ports are properly exposed
|
||||||
|
local open_ports=$(ss -tuln | grep -E ":(8080|4840|502|9090)" | wc -l)
|
||||||
|
if [ $open_ports -gt 0 ]; then
|
||||||
|
print_success "Required ports are open"
|
||||||
|
else
|
||||||
|
print_warning "Some required ports may not be open"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check backup configuration
|
||||||
|
check_backup_configuration() {
|
||||||
|
print_status "Checking backup configuration..."
|
||||||
|
|
||||||
|
if [ -f "$DEPLOYMENT_DIR/scripts/backup-full.sh" ]; then
|
||||||
|
print_success "Backup script exists: $DEPLOYMENT_DIR/scripts/backup-full.sh"
|
||||||
|
|
||||||
|
# Check if backup directory exists and is writable
|
||||||
|
if [ -w "/var/backup/calejo" ]; then
|
||||||
|
print_success "Backup directory is writable: /var/backup/calejo"
|
||||||
|
else
|
||||||
|
print_error "Backup directory is not writable: /var/backup/calejo"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Backup script not found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to generate validation report
|
||||||
|
generate_validation_report() {
|
||||||
|
print_status "Generating validation report..."
|
||||||
|
|
||||||
|
local report_file="/tmp/calejo-deployment-validation-$(date +%Y%m%d_%H%M%S).txt"
|
||||||
|
|
||||||
|
cat > "$report_file" << EOF
|
||||||
|
Calejo Control Adapter - Deployment Validation Report
|
||||||
|
Generated: $(date)
|
||||||
|
System: $(hostname)
|
||||||
|
|
||||||
|
VALIDATION CHECKS:
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Run checks and capture output
|
||||||
|
{
|
||||||
|
echo "1. System Resources:"
|
||||||
|
check_system_resources 2>&1 | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "2. Container Status:"
|
||||||
|
check_container_status 2>&1 | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "3. Application Endpoints:"
|
||||||
|
check_application_endpoints 2>&1 | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "4. Configuration:"
|
||||||
|
check_configuration 2>&1 | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "5. Logs:"
|
||||||
|
check_logs 2>&1 | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "6. Security:"
|
||||||
|
check_security 2>&1 | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "7. Backup Configuration:"
|
||||||
|
check_backup_configuration 2>&1 | sed 's/^/ /'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "SUMMARY:"
|
||||||
|
echo "Deployment validation completed. Review any warnings or errors above."
|
||||||
|
|
||||||
|
} >> "$report_file"
|
||||||
|
|
||||||
|
print_success "Validation report generated: $report_file"
|
||||||
|
|
||||||
|
# Display summary
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo " DEPLOYMENT VALIDATION SUMMARY"
|
||||||
|
echo "=================================================="
|
||||||
|
echo ""
|
||||||
|
echo "📊 System Status:"
|
||||||
|
check_system_resources | grep -E "(Disk usage|CPU load)"
|
||||||
|
echo ""
|
||||||
|
echo "🔧 Application Status:"
|
||||||
|
check_application_endpoints > /dev/null 2>&1 && echo " ✅ All endpoints accessible" || echo " ❌ Some endpoints failed"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Next Steps:"
|
||||||
|
echo " Review full report: $report_file"
|
||||||
|
echo " Address any warnings or errors"
|
||||||
|
echo " Run end-to-end tests: python test-e2e-deployment.py"
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main validation function
|
||||||
|
main() {
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Calejo Control Adapter - Deployment Validation"
|
||||||
|
echo "=================================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if application is running
|
||||||
|
if ! curl -s "$BASE_URL/health" > /dev/null 2>&1; then
|
||||||
|
print_error "Application is not running or not accessible at $BASE_URL"
|
||||||
|
echo ""
|
||||||
|
echo "Please ensure the application is running before validation."
|
||||||
|
echo "Start with: systemctl start calejo-control-adapter"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run validation checks
|
||||||
|
check_system_resources
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_container_status
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_application_endpoints
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_configuration
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_logs
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_security
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_backup_configuration
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Generate comprehensive report
|
||||||
|
generate_validation_report
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_success "Deployment validation completed!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main "$@"
|
||||||
Loading…
Reference in New Issue