ansible role

This commit is contained in:
2025-09-17 20:27:50 +10:00
parent 88f7e5bd17
commit 9fa32731be
31 changed files with 5631 additions and 11 deletions
+65 -11
View File
@@ -1,12 +1,66 @@
__pycache__
instance/*
logs/*
.venv/
mariadb_data/
.env
.aider*
worker_settings.json
worker/logs/
celerybeat-schedule
# Ansible
*.retry
.ansible/
ansible.log
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Secrets and sensitive data
vault_pass.txt
*.vault
group_vars/*/vault.yml
host_vars/*/vault.yml
# Test artifacts
test_results/
.pytest_cache/
# Distribution
dist/
*.tar.gz
*.zip
# Logs
*.log
logs/
.swp
# Temporary files
tmp/
temp/
.tmp/
+11
View File
@@ -0,0 +1,11 @@
customModes:
- slug: code-reviewer
name: Code Reviewer
roleDefinition: |
You are a senior software engineer conducting thorough code reviews. You focus on code quality, security, performance, and maintainability.
groups:
- read
- browser
customInstructions: |
Provide constructive feedback on code patterns, potential bugs, security issues, and improvement opportunities. Be specific and actionable in suggestions.
source: project
+76
View File
@@ -0,0 +1,76 @@
# Changelog
All notable changes to the xCloudify Ansible Infrastructure project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2024-01-17
### Added
- Initial release of xCloudify Ansible Infrastructure roles
- `xcloudify_infrastructure` role for container orchestration
- `xcloudify_admin` role for administrative operations
- VDC-centric approach with automatic context resolution
- Idempotent infrastructure management
- Container lifecycle management (start/stop/restart)
- Network management with VXLAN support
- Volume management with multiple storage types
- Port forwarding with automatic DNS integration
- Bearer token authentication
- Comprehensive error handling and validation
- Check mode support for dry-run operations
- Integration test suite
- Complete documentation and examples
### Features
- **Container Management**: Deploy, update, and manage Docker containers
- **Network Management**: Create and manage VXLAN networks with VNI allocation
- **Volume Management**: Persistent storage with multiple backend types
- **Port Forwarding**: External access with Cloudflare DNS integration
- **Idempotency**: Safe to run multiple times with consistent results
- **State Management**: Automatic detection and management of resource state
- **Error Handling**: Comprehensive error handling with rollback capabilities
- **Authentication**: Bearer token authentication with environment variable support
- **Validation**: Pre-flight validation of all resource specifications
- **Testing**: Complete integration test suite
### Modules
- `xcloudify_container`: Container lifecycle management
- `xcloudify_network`: Network management
- `xcloudify_volume`: Volume management
- `xcloudify_region`: Region management (admin)
- `xcloudify_vdc`: Virtual Data Center management (admin)
### Examples
- Simple web application deployment
- Multi-tier application with database
- Container lifecycle management
- Rolling updates and scaling
- Administrative operations
### Documentation
- Complete role documentation
- API integration guide
- Usage examples
- Troubleshooting guide
- Development setup
## [Unreleased]
### Planned
- Support for virtual machines
- Advanced networking features
- Load balancer integration
- Monitoring and alerting
- Backup and restore operations
- Multi-region deployments
- GitOps integration
- Terraform provider compatibility
### Under Consideration
- Kubernetes integration
- Service mesh support
- Auto-scaling capabilities
- Cost optimization features
- Compliance and security scanning
+348
View File
@@ -0,0 +1,348 @@
# Getting Started with xCloudify Ansible Infrastructure
This guide will help you get started with the xCloudify Ansible Infrastructure roles for managing containerized workloads.
## 📋 Prerequisites
Before you begin, ensure you have:
1. **Ansible installed** (version 2.9 or higher)
2. **Python 3.6+** with pip
3. **xCloudify account** with API access
4. **Virtual Data Center ID** from your xCloudify dashboard
5. **Bearer token** for API authentication
## 🔧 Installation
### Step 1: Clone the Repository
```bash
git clone https://github.com/xcloudify/ansible-infrastructure.git
cd ansible-infrastructure
```
### Step 2: Install Dependencies
```bash
# Install Ansible dependencies
make install
# Or manually:
ansible-galaxy install -r requirements.yml
pip install ansible-lint yamllint
```
### Step 3: Set Up Authentication
Choose one of the following methods:
#### Option A: Environment Variables (Recommended)
```bash
export XCLOUDIFY_BEARER_TOKEN="your-bearer-token-here"
export TEST_VDC_ID="your-vdc-id-here" # For testing
```
#### Option B: Ansible Vault
```bash
# Create encrypted vault file
ansible-vault create group_vars/all/vault.yml
# Add your credentials:
vault_xcloudify_token: "your-bearer-token-here"
vault_test_vdc_id: "your-vdc-id-here"
```
#### Option C: Direct Variables (Not Recommended for Production)
```yaml
# In your playbook
vars:
xcloudify_bearer_token: "your-bearer-token-here"
xcloudify_vdc_id: "your-vdc-id-here"
```
## 🚀 Your First Deployment
### Step 1: Validate Your Setup
```bash
# Test your credentials and VDC access
ansible-playbook tests/test-infrastructure.yml --check
```
### Step 2: Deploy a Simple Web Application
Create a playbook file `my-first-app.yml`:
```yaml
---
- name: Deploy my first xCloudify application
hosts: localhost
gather_facts: false
vars:
my_vdc_id: "{{ lookup('env', 'TEST_VDC_ID') }}"
my_token: "{{ lookup('env', 'XCLOUDIFY_BEARER_TOKEN') }}"
tasks:
- name: Deploy web application
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ my_token }}"
xcloudify_debug: true
xcloudify_infrastructure:
networks:
- name: "my-web-network"
vni: 1001
ipv4_cidr: "10.1.0.0/24"
ipv4_gateway: "10.1.0.1"
volumes:
- name: "my-web-content"
size_gb: 10
type: "local"
containers:
- name: "my-web-server"
image: "nginx:alpine"
cpu_shares: 1
mem_limit: 256
ports:
- internal: 80
external: 8080
use_dns: true
storage:
- volume: "my-web-content"
mount_point: "/usr/share/nginx/html"
networks: ["my-web-network"]
```
### Step 3: Deploy Your Application
```bash
# Preview what will be created
ansible-playbook my-first-app.yml --check
# Deploy the application
ansible-playbook my-first-app.yml
# Verify idempotency (should show no changes)
ansible-playbook my-first-app.yml
```
## 🎯 Common Use Cases
### 1. Simple Static Website
```yaml
xcloudify_infrastructure:
containers:
- name: "static-site"
image: "nginx:alpine"
ports:
- internal: 80
external: 80
use_dns: true
```
### 2. Database with Persistent Storage
```yaml
xcloudify_infrastructure:
volumes:
- name: "postgres-data"
size_gb: 100
type: "local"
containers:
- name: "database"
image: "postgres:13"
cpu_shares: 2
mem_limit: 1024
storage:
- volume: "postgres-data"
mount_point: "/var/lib/postgresql/data"
env:
POSTGRES_DB: "myapp"
POSTGRES_USER: "user"
POSTGRES_PASSWORD: "{{ vault_db_password }}"
```
### 3. Multi-Container Application
```yaml
xcloudify_infrastructure:
networks:
- name: "app-network"
vni: 2001
ipv4_cidr: "10.2.0.0/24"
containers:
- name: "frontend"
image: "nginx:latest"
ports:
- internal: 80
external: 80
use_dns: true
networks: ["app-network"]
- name: "backend"
image: "myapp:latest"
ports:
- internal: 3000
external: 3000
networks: ["app-network"]
- name: "redis"
image: "redis:alpine"
networks: ["app-network"]
```
## 🔄 Lifecycle Management
### Starting and Stopping Containers
```yaml
# Stop containers
- name: Stop application
xcloudify_container:
vdc_id: "{{ my_vdc_id }}"
bearer_token: "{{ my_token }}"
containers:
- name: "my-web-server"
state: stopped
# Start containers
- name: Start application
xcloudify_container:
vdc_id: "{{ my_vdc_id }}"
bearer_token: "{{ my_token }}"
containers:
- name: "my-web-server"
state: started
# Restart containers
- name: Restart application
xcloudify_container:
vdc_id: "{{ my_vdc_id }}"
bearer_token: "{{ my_token }}"
containers:
- name: "my-web-server"
state: restarted
```
### Updating Applications
```yaml
# Update container image (triggers recreation)
- name: Update to new version
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ my_token }}"
xcloudify_infrastructure:
containers:
- name: "my-web-server"
image: "nginx:1.21-alpine" # New version
cpu_shares: 2 # Increased resources
mem_limit: 512
# ... rest of configuration
```
## 🧪 Testing Your Setup
### Run the Test Suite
```bash
# Set up test environment
export TEST_VDC_ID="your-test-vdc-id"
export XCLOUDIFY_BEARER_TOKEN="your-token"
# Run comprehensive tests
make test
# Or manually:
ansible-playbook tests/test-infrastructure.yml
```
### Manual Testing
```bash
# Test with check mode (dry run)
ansible-playbook my-first-app.yml --check
# Deploy and verify
ansible-playbook my-first-app.yml
# Test idempotency
ansible-playbook my-first-app.yml # Should show no changes
# Clean up
ansible-playbook my-first-app.yml -e "state=absent"
```
## 🔍 Troubleshooting
### Common Issues
#### Authentication Errors
```
Error: xCloudify API request failed: 401
```
**Solution**: Check your bearer token and ensure it has the correct permissions.
#### VDC Not Found
```
Error: Virtual Data Center not found
```
**Solution**: Verify your VDC ID and ensure you have access to it.
#### Network VNI Conflicts
```
Error: VNI already in use
```
**Solution**: Use unique VNI values for each network in your region.
#### Volume Attachment Errors
```
Error: Volume is already attached to workloads
```
**Solution**: Detach volume from existing workloads before deletion.
### Debug Mode
Enable detailed logging:
```yaml
xcloudify_debug: true
xcloudify_log_api_calls: true
```
### Check API Connectivity
```bash
# Test API connectivity
curl -H "Authorization: Bearer $XCLOUDIFY_BEARER_TOKEN" \
https://api.xcloudify.tech/api/virtual_data_centers/$TEST_VDC_ID
```
## 📚 Next Steps
1. **Explore Examples**: Check out the [`examples/`](examples/) directory for more complex scenarios
2. **Read Documentation**: Review the complete [`README.md`](README.md) for detailed information
3. **Join Community**: Connect with other users in the xCloudify community
4. **Contribute**: Help improve the roles by submitting issues and pull requests
## 🆘 Getting Help
- **Documentation**: [xCloudify Docs](https://docs.xcloudify.tech)
- **Issues**: [GitHub Issues](https://github.com/xcloudify/ansible-infrastructure/issues)
- **Community**: [xCloudify Community](https://community.xcloudify.tech)
- **Support**: [Support Portal](https://support.xcloudify.tech)
## 🎉 Success!
You're now ready to manage your xCloudify infrastructure with Ansible! Start with simple deployments and gradually build more complex infrastructure as you become familiar with the platform.
Happy automating! 🚀
+110
View File
@@ -0,0 +1,110 @@
# Makefile for xCloudify Ansible Infrastructure
.PHONY: help install test lint clean package deploy-test
# Default target
help:
@echo "xCloudify Ansible Infrastructure Management"
@echo ""
@echo "Available targets:"
@echo " install - Install dependencies and set up development environment"
@echo " test - Run integration tests"
@echo " lint - Run linting and validation"
@echo " clean - Clean up test artifacts"
@echo " package - Package roles for distribution"
@echo " deploy-test - Deploy test infrastructure"
@echo " help - Show this help message"
# Install dependencies
install:
@echo "Installing Ansible dependencies..."
ansible-galaxy install -r requirements.yml
@echo "Installing Python dependencies..."
pip install ansible-lint yamllint
@echo "Setup complete!"
# Run tests
test:
@echo "Running xCloudify infrastructure tests..."
@if [ -z "$$TEST_VDC_ID" ]; then \
echo "ERROR: TEST_VDC_ID environment variable not set"; \
exit 1; \
fi
@if [ -z "$$XCLOUDIFY_BEARER_TOKEN" ]; then \
echo "ERROR: XCLOUDIFY_BEARER_TOKEN environment variable not set"; \
exit 1; \
fi
ansible-playbook tests/test-infrastructure.yml
# Run linting
lint:
@echo "Running ansible-lint..."
ansible-lint roles/
@echo "Running yamllint..."
yamllint -d relaxed .
@echo "Linting complete!"
# Clean up
clean:
@echo "Cleaning up test artifacts..."
rm -f ansible.log
rm -rf .ansible/
@echo "Cleanup complete!"
# Package roles
package:
@echo "Packaging xCloudify roles..."
mkdir -p dist/
tar -czf dist/xcloudify-infrastructure-$(shell date +%Y%m%d-%H%M%S).tar.gz \
roles/xcloudify_infrastructure/ \
examples/ \
README.md \
requirements.yml \
ansible.cfg
tar -czf dist/xcloudify-admin-$(shell date +%Y%m%d-%H%M%S).tar.gz \
roles/xcloudify_admin/ \
README.md \
requirements.yml \
ansible.cfg
@echo "Packages created in dist/"
# Deploy test infrastructure
deploy-test:
@echo "Deploying test infrastructure..."
@if [ -z "$$TEST_VDC_ID" ]; then \
echo "ERROR: TEST_VDC_ID environment variable not set"; \
exit 1; \
fi
ansible-playbook examples/simple-web-app.yml \
-e "my_vdc_id=$$TEST_VDC_ID" \
-e "xcloudify_debug=true"
# Quick validation
validate:
@echo "Validating role structure..."
@for role in roles/*/; do \
echo "Checking $$role..."; \
if [ ! -f "$$role/meta/main.yml" ]; then \
echo "ERROR: Missing meta/main.yml in $$role"; \
exit 1; \
fi; \
if [ ! -f "$$role/tasks/main.yml" ]; then \
echo "ERROR: Missing tasks/main.yml in $$role"; \
exit 1; \
fi; \
done
@echo "Role structure validation complete!"
# Development helpers
dev-setup:
@echo "Setting up development environment..."
python -m venv venv
. venv/bin/activate && pip install ansible ansible-lint yamllint
@echo "Development environment ready!"
@echo "Activate with: source venv/bin/activate"
# Galaxy publishing (when ready)
publish:
@echo "Publishing to Ansible Galaxy..."
ansible-galaxy role import --api-key $$GALAXY_API_KEY xcloudify ansible-infrastructure
@echo "Published to Galaxy!"
+463
View File
@@ -0,0 +1,463 @@
# xCloudify Ansible Infrastructure as Code
Complete Ansible automation for xCloudify container orchestration platform. This repository provides idempotent infrastructure-as-code capabilities for managing Docker containers, storage volumes, and networking in xCloudify Virtual Data Centers.
## 🚀 Quick Start
### Prerequisites
- Ansible >= 2.9
- Python >= 3.6
- xCloudify account with API access
- Valid Virtual Data Center ID
### Installation
```bash
# Clone the repository
git clone https://github.com/xcloudify/ansible-infrastructure.git
cd ansible-infrastructure
# Install dependencies
ansible-galaxy install -r requirements.yml
# Set up authentication
export XCLOUDIFY_BEARER_TOKEN="your-bearer-token"
export TEST_VDC_ID="your-vdc-id"
```
### Basic Usage
```bash
# Deploy simple web application
ansible-playbook examples/simple-web-app.yml
# Deploy multi-tier application
ansible-playbook examples/multi-tier-app.yml
# Test lifecycle management
ansible-playbook examples/lifecycle-management.yml
# Run integration tests
ansible-playbook tests/test-infrastructure.yml
```
## 📦 Roles
### xcloudify_infrastructure
Main role for infrastructure management within existing Virtual Data Centers.
**Key Features:**
- VDC-centric approach (only requires VDC ID)
- Automatic context resolution (Universe/Project/Region)
- Idempotent operations
- Container lifecycle management
- Network and volume management
- Port forwarding with DNS integration
**Usage:**
```yaml
- include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ vault_token }}"
xcloudify_infrastructure:
containers:
- name: "web-app"
image: "nginx:latest"
ports:
- internal: 80
external: 8080
use_dns: true
```
### xcloudify_admin
Administrative role for managing regions and Virtual Data Centers.
**Key Features:**
- Region creation and management
- VDC creation and management
- Administrative operations
- Requires elevated privileges
**Usage:**
```yaml
- include_role:
name: xcloudify_admin
vars:
xcloudify_admin_token: "{{ admin_token }}"
xcloudify_admin_operations:
regions:
- name: "US East 1"
country: "United States"
abbreviation: "USE1"
```
## 🏗️ Architecture
### Infrastructure Hierarchy
```
Universe (Admin)
└── Project (Admin)
└── Virtual Data Center (User Entry Point)
├── Networks (VXLAN with VNI)
├── Volumes (Persistent Storage)
└── Containers (Docker Workloads)
├── Port Forwarding
├── DNS Records
└── Volume Mounts
```
### Role Structure
```
roles/
├── xcloudify_infrastructure/ # Main user role
│ ├── library/ # Custom Ansible modules
│ │ ├── xcloudify_container.py
│ │ ├── xcloudify_network.py
│ │ └── xcloudify_volume.py
│ ├── module_utils/ # Shared utilities
│ │ └── xcloudify_client.py
│ ├── tasks/ # Task orchestration
│ └── handlers/ # Event handlers
│
└── xcloudify_admin/ # Admin operations
├── library/
│ ├── xcloudify_region.py
│ └── xcloudify_vdc.py
└── tasks/
```
## 🔧 Configuration
### Authentication
Multiple authentication methods supported:
```yaml
# Method 1: Direct token
xcloudify_bearer_token: "your-token"
# Method 2: Ansible Vault
xcloudify_bearer_token: "{{ vault_xcloudify_token }}"
# Method 3: Environment variable
export XCLOUDIFY_BEARER_TOKEN="your-token"
```
### API Configuration
```yaml
# Default configuration
xcloudify_api_url: "https://api.xcloudify.tech"
xcloudify_api_timeout: 30
xcloudify_validate_ssl: true
# Custom API endpoint
xcloudify_api_url: "https://custom.xcloudify.com"
```
## 📋 Infrastructure Specification
### Complete Example
```yaml
xcloudify_infrastructure:
# Network definitions
networks:
- name: "web-tier"
vni: 1001
ipv4_cidr: "10.1.0.0/24"
ipv4_gateway: "10.1.0.1"
ipv4_dns_servers: "8.8.8.8,8.8.4.4"
- name: "app-tier"
vni: 1002
ipv4_cidr: "10.1.1.0/24"
ipv4_gateway: "10.1.1.1"
- name: "db-tier"
vni: 1003
ipv4_cidr: "10.1.2.0/24"
ipv4_gateway: "10.1.2.1"
# Volume definitions
volumes:
- name: "web-content"
size_gb: 50
type: "local"
description: "Web content storage"
- name: "app-data"
size_gb: 100
type: "local"
description: "Application data"
- name: "database-storage"
size_gb: 500
type: "local"
description: "Database storage"
# Container definitions
containers:
# Load balancer
- name: "nginx-lb"
image: "nginx:alpine"
cpu_shares: 1
mem_limit: 256
ports:
- internal: 80
external: 80
use_dns: true
- internal: 443
external: 443
use_dns: true
storage:
- volume: "web-content"
mount_point: "/usr/share/nginx/html"
read_only: true
networks: ["web-tier", "app-tier"]
env:
NGINX_WORKER_PROCESSES: "auto"
NGINX_WORKER_CONNECTIONS: "1024"
# Application servers
- name: "app-server-1"
image: "myapp:latest"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 3000
external: 3001
storage:
- volume: "app-data"
mount_point: "/app/data"
networks: ["app-tier", "db-tier"]
env:
NODE_ENV: "production"
DB_HOST: "database"
REDIS_HOST: "redis"
- name: "app-server-2"
image: "myapp:latest"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 3000
external: 3002
storage:
- volume: "app-data"
mount_point: "/app/data"
networks: ["app-tier", "db-tier"]
env:
NODE_ENV: "production"
DB_HOST: "database"
REDIS_HOST: "redis"
# Database
- name: "database"
image: "postgres:13"
cpu_shares: 2
mem_limit: 1024
storage:
- volume: "database-storage"
mount_point: "/var/lib/postgresql/data"
networks: ["db-tier"]
env:
POSTGRES_DB: "myapp"
POSTGRES_USER: "appuser"
POSTGRES_PASSWORD: "{{ vault_db_password }}"
# Cache
- name: "redis"
image: "redis:7-alpine"
cpu_shares: 1
mem_limit: 256
networks: ["db-tier"]
env:
REDIS_PASSWORD: "{{ vault_redis_password }}"
```
## 🔄 Idempotency
The roles implement comprehensive idempotency:
- **State Detection**: Checks existing resources before operations
- **Change Detection**: Only modifies resources that have changed
- **Container Recreation**: Automatically recreates containers when image/config changes
- **Resource Dependencies**: Handles dependencies between networks, volumes, and containers
### Idempotency Examples
```bash
# First run - creates everything
ansible-playbook deploy.yml
# CHANGED: Networks=3, Volumes=3, Containers=5
# Second run - no changes
ansible-playbook deploy.yml
# OK: Networks=3, Volumes=3, Containers=5
# Third run with image update - recreates containers
# (after updating image version in vars)
ansible-playbook deploy.yml
# CHANGED: Containers=5 (recreated)
```
## 🧪 Testing
### Integration Tests
```bash
# Set test environment
export TEST_VDC_ID="your-test-vdc-id"
export XCLOUDIFY_BEARER_TOKEN="your-token"
# Run full test suite
ansible-playbook tests/test-infrastructure.yml
# Run with check mode
ansible-playbook tests/test-infrastructure.yml --check
```
### Manual Testing
```bash
# Test individual components
ansible-playbook -e "xcloudify_vdc_id=your-vdc" examples/simple-web-app.yml --check
ansible-playbook -e "xcloudify_vdc_id=your-vdc" examples/simple-web-app.yml
ansible-playbook -e "xcloudify_vdc_id=your-vdc" examples/simple-web-app.yml --check # Should show no changes
```
## 📚 Examples
### Simple Deployment
```yaml
# inventory/group_vars/all.yml
xcloudify_vdc_id: "550e8400-e29b-41d4-a716-446655440000"
xcloudify_bearer_token: "{{ vault_xcloudify_token }}"
# playbooks/deploy-web.yml
- hosts: localhost
roles:
- role: xcloudify_infrastructure
vars:
xcloudify_infrastructure:
containers:
- name: "my-web-app"
image: "nginx:latest"
ports:
- internal: 80
external: 8080
use_dns: true
```
### Advanced Deployment
See [`examples/multi-tier-app.yml`](examples/multi-tier-app.yml) for a complete multi-tier application example.
## 🔒 Security
### Best Practices
- Store bearer tokens in Ansible Vault
- Use environment variables for CI/CD
- Validate SSL certificates in production
- Limit API token permissions
- Use separate tokens for admin operations
### Vault Setup
```bash
# Create vault file
ansible-vault create group_vars/all/vault.yml
# Add token to vault
vault_xcloudify_token: "your-bearer-token"
vault_db_password: "your-db-password"
```
## 🐛 Troubleshooting
### Common Issues
1. **Authentication Errors**
```
Error: xCloudify API request failed: 401
Solution: Check bearer token and permissions
```
2. **VDC Not Found**
```
Error: Virtual Data Center not found
Solution: Verify VDC ID and access permissions
```
3. **Resource Conflicts**
```
Error: VNI already in use
Solution: Use unique VNI values for networks
```
### Debug Mode
```yaml
# Enable detailed logging
xcloudify_debug: true
xcloudify_log_api_calls: true
```
### Check Mode
```bash
# Preview changes without applying
ansible-playbook deploy.yml --check --diff
```
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Ensure all tests pass
5. Submit a pull request
### Development Setup
```bash
# Install development dependencies
pip install -r requirements-dev.txt
# Run linting
ansible-lint roles/
# Run tests
ansible-playbook tests/test-infrastructure.yml
```
## 📄 License
MIT License - see LICENSE file for details.
## 👥 Support
- Documentation: [xCloudify Docs](https://docs.xcloudify.tech)
- Issues: [GitHub Issues](https://github.com/xcloudify/ansible-infrastructure/issues)
- Community: [xCloudify Community](https://community.xcloudify.tech)
## 🏷️ Version History
- **v1.0.0**: Initial release with core functionality
- Container management
- Network management
- Volume management
- Port forwarding
- DNS integration
- Idempotent operations
- Check mode support
+29
View File
@@ -0,0 +1,29 @@
[defaults]
# Basic configuration for xCloudify Ansible roles
host_key_checking = False
retry_files_enabled = False
gathering = explicit
roles_path = ./roles
library = ./roles/xcloudify_infrastructure/library:./roles/xcloudify_admin/library
module_utils = ./roles/xcloudify_infrastructure/module_utils
# Logging
log_path = ./ansible.log
display_skipped_hosts = False
display_ok_hosts = True
# Performance
forks = 10
timeout = 30
command_timeout = 30
# Output formatting
stdout_callback = yaml
bin_ansible_callbacks = True
[inventory]
enable_plugins = host_list, script, auto, yaml, ini, toml
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
+198
View File
@@ -0,0 +1,198 @@
---
# Container lifecycle management examples
- name: Container lifecycle management examples
hosts: localhost
gather_facts: false
vars:
my_vdc_id: "550e8400-e29b-41d4-a716-446655440000"
xcloudify_token: "{{ vault_xcloudify_token | default(lookup('env', 'XCLOUDIFY_BEARER_TOKEN')) }}"
tasks:
# Example 1: Deploy new containers
- name: Deploy application containers
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ xcloudify_token }}"
xcloudify_infrastructure:
containers:
- name: "web-app"
image: "nginx:latest"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 80
external: 8080
use_dns: true
- name: "api-server"
image: "myapi:latest"
cpu_shares: 1
mem_limit: 256
ports:
- internal: 3000
external: 3000
# Example 2: Stop containers for maintenance
- name: Stop containers for maintenance
xcloudify_container:
vdc_id: "{{ my_vdc_id }}"
bearer_token: "{{ xcloudify_token }}"
containers:
- name: "web-app"
- name: "api-server"
state: stopped
# Example 3: Start containers after maintenance
- name: Start containers after maintenance
xcloudify_container:
vdc_id: "{{ my_vdc_id }}"
bearer_token: "{{ xcloudify_token }}"
containers:
- name: "web-app"
- name: "api-server"
state: started
# Example 4: Restart specific container
- name: Restart web application
xcloudify_container:
vdc_id: "{{ my_vdc_id }}"
bearer_token: "{{ xcloudify_token }}"
containers:
- name: "web-app"
state: restarted
# Example 5: Update container image (triggers recreation)
- name: Update application to new version
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ xcloudify_token }}"
xcloudify_infrastructure:
containers:
- name: "web-app"
image: "nginx:1.21-alpine" # New image version
cpu_shares: 2
mem_limit: 512
ports:
- internal: 80
external: 8080
use_dns: true
# Example 6: Scale application (add more containers)
- name: Scale application horizontally
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ xcloudify_token }}"
xcloudify_infrastructure:
containers:
- name: "web-app-1"
image: "nginx:latest"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 80
external: 8081
use_dns: true
- name: "web-app-2"
image: "nginx:latest"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 80
external: 8082
use_dns: true
- name: "web-app-3"
image: "nginx:latest"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 80
external: 8083
use_dns: true
# Example 7: Remove old containers
- name: Remove old containers
xcloudify_container:
vdc_id: "{{ my_vdc_id }}"
bearer_token: "{{ xcloudify_token }}"
containers:
- name: "old-web-app"
- name: "deprecated-service"
state: absent
# Example 8: Health check and conditional restart
- name: Check container health
uri:
url: "{{ xcloudify_api_url | default('https://api.xcloudify.tech') }}/api/workloads/containers"
method: GET
headers:
Authorization: "Bearer {{ xcloudify_token }}"
status_code: 200
register: container_health
- name: Restart unhealthy containers
xcloudify_container:
vdc_id: "{{ my_vdc_id }}"
bearer_token: "{{ xcloudify_token }}"
containers:
- name: "{{ item.name }}"
state: restarted
loop: "{{ container_health.json.data | default([]) }}"
when:
- item.status in ['dead', 'error', 'launch_failed']
- item.vdc_id == my_vdc_id
# Example 9: Rolling update with zero downtime
- name: Rolling update - deploy new version
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ xcloudify_token }}"
xcloudify_infrastructure:
containers:
- name: "web-app-new"
image: "nginx:1.21-alpine"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 80
external: 8090
use_dns: true
- name: Wait for new container to be healthy
uri:
url: "http://web-app-new.example.com/health"
method: GET
status_code: 200
retries: 30
delay: 10
- name: Remove old container after successful deployment
xcloudify_container:
vdc_id: "{{ my_vdc_id }}"
bearer_token: "{{ xcloudify_token }}"
containers:
- name: "web-app"
state: absent
- name: Rename new container to production name
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ xcloudify_token }}"
xcloudify_infrastructure:
containers:
- name: "web-app"
image: "nginx:1.21-alpine"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 80
external: 8080
use_dns: true
+182
View File
@@ -0,0 +1,182 @@
---
# Multi-tier application deployment example
- name: Deploy multi-tier application to xCloudify
hosts: localhost
gather_facts: false
vars:
# VDC ID - replace with your actual VDC ID
production_vdc: "550e8400-e29b-41d4-a716-446655440000"
# Bearer token - use vault or environment variable
xcloudify_token: "{{ vault_xcloudify_token | default(lookup('env', 'XCLOUDIFY_BEARER_TOKEN')) }}"
# Application configuration
app_name: "myapp"
app_version: "v1.2.3"
db_password: "{{ vault_db_password }}"
tasks:
- name: Deploy complete application stack
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ production_vdc }}"
xcloudify_bearer_token: "{{ xcloudify_token }}"
xcloudify_debug: true
xcloudify_wait_for_deployment: true
xcloudify_deployment_timeout: 600
xcloudify_infrastructure:
networks:
- name: "{{ app_name }}-frontend"
vni: 3001
ipv4_cidr: "10.3.0.0/24"
ipv4_gateway: "10.3.0.1"
ipv4_dns_servers: "8.8.8.8,8.8.4.4"
description: "Frontend network for {{ app_name }}"
- name: "{{ app_name }}-backend"
vni: 3002
ipv4_cidr: "10.3.1.0/24"
ipv4_gateway: "10.3.1.1"
ipv4_dns_servers: "8.8.8.8,8.8.4.4"
description: "Backend network for {{ app_name }}"
- name: "{{ app_name }}-database"
vni: 3003
ipv4_cidr: "10.3.2.0/24"
ipv4_gateway: "10.3.2.1"
description: "Database network for {{ app_name }}"
volumes:
- name: "{{ app_name }}-db-data"
size_gb: 200
type: "local"
description: "Database data for {{ app_name }}"
- name: "{{ app_name }}-app-logs"
size_gb: 50
type: "local"
description: "Application logs for {{ app_name }}"
- name: "{{ app_name }}-nginx-config"
size_gb: 5
type: "local"
description: "Nginx configuration for {{ app_name }}"
- name: "{{ app_name }}-app-uploads"
size_gb: 100
type: "local"
description: "User uploads for {{ app_name }}"
containers:
# Database tier
- name: "{{ app_name }}-database"
image: "postgres:13-alpine"
cpu_shares: 2
mem_limit: 1024
storage:
- volume: "{{ app_name }}-db-data"
mount_point: "/var/lib/postgresql/data"
networks: ["{{ app_name }}-database"]
env:
POSTGRES_DB: "{{ app_name }}"
POSTGRES_USER: "{{ app_name }}_user"
POSTGRES_PASSWORD: "{{ db_password }}"
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
# Redis cache
- name: "{{ app_name }}-redis"
image: "redis:7-alpine"
cpu_shares: 1
mem_limit: 256
networks: ["{{ app_name }}-backend"]
env:
REDIS_PASSWORD: "{{ vault_redis_password | default('') }}"
# Application backend
- name: "{{ app_name }}-backend"
image: "{{ app_name }}:{{ app_version }}"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 3000
external: 3000
storage:
- volume: "{{ app_name }}-app-logs"
mount_point: "/app/logs"
- volume: "{{ app_name }}-app-uploads"
mount_point: "/app/uploads"
networks: ["{{ app_name }}-backend", "{{ app_name }}-database"]
env:
NODE_ENV: "production"
DB_HOST: "{{ app_name }}-database"
DB_NAME: "{{ app_name }}"
DB_USER: "{{ app_name }}_user"
DB_PASSWORD: "{{ db_password }}"
REDIS_HOST: "{{ app_name }}-redis"
REDIS_PORT: "6379"
LOG_LEVEL: "info"
# Frontend proxy
- name: "{{ app_name }}-frontend"
image: "nginx:alpine"
cpu_shares: 1
mem_limit: 256
ports:
- internal: 80
external: 80
use_dns: true
- internal: 443
external: 443
use_dns: true
storage:
- volume: "{{ app_name }}-nginx-config"
mount_point: "/etc/nginx/conf.d"
read_only: true
networks: ["{{ app_name }}-frontend", "{{ app_name }}-backend"]
env:
NGINX_HOST: "{{ app_name }}.example.com"
BACKEND_HOST: "{{ app_name }}-backend"
BACKEND_PORT: "3000"
- name: Wait for application to be healthy
uri:
url: "http://{{ app_name }}.example.com/health"
method: GET
status_code: 200
register: health_check
retries: 30
delay: 10
until: health_check.status == 200
when: not ansible_check_mode
- name: Display deployment summary
debug:
msg: |
🚀 {{ app_name | upper }} DEPLOYMENT COMPLETED SUCCESSFULLY! 🚀
📊 Infrastructure Summary:
- VDC: {{ _xcloudify_vdc_context.vdc.name }}
- Region: {{ _xcloudify_vdc_context.region.name }}
- Networks: {{ _xcloudify_created_networks.keys() | list | length }}
- Volumes: {{ _xcloudify_created_volumes.keys() | list | length }}
- Containers: {{ _xcloudify_created_containers.keys() | list | length }}
🌐 Access Points:
- Frontend: http://{{ app_name }}.example.com
- Backend API: http://{{ app_name }}.example.com:3000
📦 Deployed Containers:
{% for name, container in _xcloudify_created_containers.items() %}
- {{ name }}: {{ container.container_status | default('unknown') }}
{% endfor %}
💾 Storage Volumes:
{% for name, volume in _xcloudify_created_volumes.items() %}
- {{ name }}: {{ volume.size_gb }}GB ({{ volume.type }})
{% endfor %}
🔗 Networks:
{% for name, network in _xcloudify_created_networks.items() %}
- {{ name }}: {{ network.ipv4_cidr | default('No CIDR') }}
{% endfor %}
+77
View File
@@ -0,0 +1,77 @@
---
# Simple web application deployment example
- name: Deploy simple web application to xCloudify
hosts: localhost
gather_facts: false
vars:
# VDC ID - replace with your actual VDC ID
my_vdc_id: "550e8400-e29b-41d4-a716-446655440000"
# Bearer token - use vault or environment variable
xcloudify_token: "{{ vault_xcloudify_token | default(lookup('env', 'XCLOUDIFY_BEARER_TOKEN')) }}"
tasks:
- name: Deploy web application infrastructure
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ xcloudify_token }}"
xcloudify_debug: true
xcloudify_infrastructure:
networks:
- name: "web-network"
vni: 1001
ipv4_cidr: "10.1.0.0/24"
ipv4_gateway: "10.1.0.1"
ipv4_dns_servers: "8.8.8.8,8.8.4.4"
description: "Web application network"
volumes:
- name: "web-content"
size_gb: 50
type: "local"
description: "Web content storage"
- name: "web-logs"
size_gb: 10
type: "local"
description: "Web server logs"
containers:
- name: "nginx-web"
image: "nginx:alpine"
cpu_shares: 1
mem_limit: 256
ports:
- internal: 80
external: 8080
use_dns: true
storage:
- volume: "web-content"
mount_point: "/usr/share/nginx/html"
- volume: "web-logs"
mount_point: "/var/log/nginx"
networks: ["web-network"]
env:
NGINX_HOST: "myapp.example.com"
NGINX_PORT: "80"
- name: Display deployment summary
debug:
msg: |
Deployment completed successfully!
Networks created:
{% for network in _xcloudify_created_networks.values() %}
- {{ network.name }} ({{ network.ipv4_cidr | default('No CIDR') }})
{% endfor %}
Volumes created:
{% for volume in _xcloudify_created_volumes.values() %}
- {{ volume.name }} ({{ volume.size_gb }}GB)
{% endfor %}
Containers deployed:
{% for container in _xcloudify_created_containers.values() %}
- {{ container.name }} ({{ container.container_status | default('unknown') }})
{% endfor %}
+14
View File
@@ -0,0 +1,14 @@
---
# Ansible Galaxy requirements for xCloudify roles
collections:
- name: community.general
version: ">=3.0.0"
- name: ansible.posix
version: ">=1.0.0"
roles: []
# Python requirements
python:
- requests>=2.25.0
- urllib3>=1.26.0
@@ -0,0 +1,481 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, xCloudify Team
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: xcloudify_region
short_description: Manage regions in xCloudify (Admin Only)
description:
- Create, update, or delete regions in xCloudify
- Administrative operation requiring elevated privileges
- Supports idempotent operations with automatic state detection
version_added: "1.0.0"
options:
bearer_token:
description: Bearer token for authentication (admin required)
required: true
type: str
api_url:
description: xCloudify API URL
required: false
default: https://api.xcloudify.tech
type: str
regions:
description: List of region specifications
required: true
type: list
elements: dict
suboptions:
name:
description: Region name
required: true
type: str
country:
description: Country name
required: true
type: str
abbreviation:
description: Region abbreviation (max 16 chars)
required: true
type: str
description:
description: Region description
required: false
type: str
private_region:
description: Whether this is a private region
required: false
default: false
type: bool
advertised_address:
description: Advertised address for the region
required: false
type: str
ip_address_range_northsouth:
description: North-south IP address range (CIDR)
required: false
type: str
ip_address_range_eastwest:
description: East-west IP address range (CIDR)
required: false
type: str
state:
description: Desired state
required: false
default: present
choices: ['present', 'absent']
type: str
api_timeout:
description: API request timeout in seconds
required: false
default: 30
type: int
debug:
description: Enable debug logging
required: false
default: false
type: bool
author:
- xCloudify Team
'''
EXAMPLES = '''
- name: Create regions
xcloudify_region:
bearer_token: "{{ admin_token }}"
regions:
- name: "US East 1"
country: "United States"
abbreviation: "USE1"
description: "Primary US East Coast region"
ip_address_range_northsouth: "203.0.113.0/24"
ip_address_range_eastwest: "10.1.0.0/16"
- name: "EU West 1"
country: "Ireland"
abbreviation: "EUW1"
description: "Primary EU West region"
private_region: false
- name: Remove regions
xcloudify_region:
bearer_token: "{{ admin_token }}"
regions:
- name: "Old Region"
state: absent
'''
RETURN = '''
regions:
description: List of region results
returned: always
type: list
elements: dict
contains:
name:
description: Region name
type: str
id:
description: Region ID
type: str
status:
description: Operation status (created, updated, unchanged, deleted, not_found)
type: str
country:
description: Country name
type: str
abbreviation:
description: Region abbreviation
type: str
enrollment_key:
description: Region enrollment key
type: str
details:
description: Full region details from API
type: dict
changed:
description: Whether any changes were made
returned: always
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
import json
import os
import ipaddress
class XCloudifyAdminAPIError(Exception):
"""Custom exception for xCloudify Admin API errors"""
def __init__(self, message, status_code=None, response_data=None):
super(XCloudifyAdminAPIError, self).__init__(message)
self.status_code = status_code
self.response_data = response_data
class XCloudifyAdminClient:
"""xCloudify Admin API client"""
def __init__(self, module, api_url, bearer_token):
self.module = module
self.api_url = api_url.rstrip('/')
self.bearer_token = bearer_token
self.headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {bearer_token}'
}
self.timeout = getattr(module.params, 'api_timeout', 30)
self.debug = getattr(module.params, 'debug', False)
def _make_request(self, method, endpoint, data=None):
"""Make HTTP request to xCloudify API"""
url = f"{self.api_url}/api/{endpoint}"
if self.debug:
self.module.debug(f"xCloudify Admin API {method} {url}")
if data:
self.module.debug(f"Request data: {json.dumps(data, indent=2)}")
if data:
data = json.dumps(data)
response, info = fetch_url(
self.module,
url,
method=method,
headers=self.headers,
data=data,
timeout=self.timeout
)
if info['status'] >= 400:
error_msg = f"xCloudify Admin API request failed: {info['status']}"
error_data = None
if response:
try:
error_data = json.loads(response.read())
if 'message' in error_data:
error_msg += f" - {error_data['message']}"
except:
pass
raise XCloudifyAdminAPIError(
error_msg,
status_code=info['status'],
response_data=error_data
)
if response:
result = json.loads(response.read())
if self.debug:
self.module.debug(f"Response data: {json.dumps(result, indent=2)}")
# Handle xCloudify API response envelope
if isinstance(result, dict) and 'success' in result:
if result['success']:
return result.get('data')
else:
raise XCloudifyAdminAPIError(
f"xCloudify Admin API error: {result.get('message', 'Unknown error')}",
response_data=result.get('error_details', {})
)
return result
return None
def list_regions(self):
"""List all regions"""
return self._make_request('GET', 'regions') or []
def ensure_region(self, region_spec):
"""Idempotent region management"""
regions = self.list_regions()
existing = None
for region in regions:
if region['name'] == region_spec['name']:
existing = region
break
if not existing:
# Create region
region_data = {
'name': region_spec['name'],
'country': region_spec['country'],
'abbreviation': region_spec['abbreviation'],
'description': region_spec.get('description', f"Region {region_spec['name']}"),
'private_region': region_spec.get('private_region', False),
'advertised_address': region_spec.get('advertised_address'),
'ip_address_range_northsouth': region_spec.get('ip_address_range_northsouth'),
'ip_address_range_eastwest': region_spec.get('ip_address_range_eastwest')
}
# Remove None values
region_data = {k: v for k, v in region_data.items() if v is not None}
result = self._make_request('POST', 'regions', region_data)
return result, True # created
else:
# Check if update needed
needs_update = False
update_data = {}
for field in ['country', 'abbreviation', 'description', 'private_region',
'advertised_address', 'ip_address_range_northsouth', 'ip_address_range_eastwest']:
if field in region_spec and existing.get(field) != region_spec[field]:
needs_update = True
update_data[field] = region_spec[field]
if needs_update:
result = self._make_request('PUT', f'regions/{existing["id"]}', update_data)
return result, True # updated
return existing, False # no change
def delete_region(self, region_name):
"""Delete a region by name"""
regions = self.list_regions()
for region in regions:
if region['name'] == region_name:
self._make_request('DELETE', f'regions/{region["id"]}')
return True
return False # not found
def validate_region_spec(region_spec):
"""Validate region specification"""
errors = []
if not region_spec.get('name'):
errors.append("Region name is required")
if not region_spec.get('country'):
errors.append("Country is required")
if not region_spec.get('abbreviation'):
errors.append("Abbreviation is required")
elif len(region_spec['abbreviation']) > 16:
errors.append("Abbreviation must be 16 characters or less")
# Validate IP ranges if provided
for field in ['ip_address_range_northsouth', 'ip_address_range_eastwest']:
if field in region_spec and region_spec[field]:
try:
ipaddress.ip_network(region_spec[field])
except ValueError:
errors.append(f"Invalid CIDR format for {field}: {region_spec[field]}")
return errors
def main():
module_args = dict(
bearer_token=dict(type='str', required=True, no_log=True),
api_url=dict(type='str', required=False, default='https://api.xcloudify.tech'),
regions=dict(type='list', required=True, elements='dict'),
state=dict(type='str', default='present', choices=['present', 'absent']),
api_timeout=dict(type='int', default=30),
debug=dict(type='bool', default=False)
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# Validate region specifications
for region_spec in module.params['regions']:
errors = validate_region_spec(region_spec)
if errors:
module.fail_json(
msg=f"Invalid region specification for '{region_spec.get('name', 'unnamed')}'",
errors=errors
)
try:
client = XCloudifyAdminClient(
module,
module.params['api_url'],
module.params['bearer_token']
)
changed = False
results = []
for region_spec in module.params['regions']:
try:
if module.params['state'] == 'present':
if module.check_mode:
# In check mode, just validate and report what would be done
existing_regions = client.list_regions()
existing = next((r for r in existing_regions if r['name'] == region_spec['name']), None)
if not existing:
results.append({
'name': region_spec['name'],
'id': None,
'status': 'would_create',
'country': region_spec['country'],
'abbreviation': region_spec['abbreviation'],
'enrollment_key': None,
'details': {}
})
changed = True
else:
# Check if update would be needed
needs_update = False
for field in ['country', 'abbreviation', 'description', 'private_region']:
if field in region_spec and existing.get(field) != region_spec[field]:
needs_update = True
break
results.append({
'name': region_spec['name'],
'id': existing['id'],
'status': 'would_update' if needs_update else 'unchanged',
'country': existing['country'],
'abbreviation': existing['abbreviation'],
'enrollment_key': existing.get('enrollment_key'),
'details': existing
})
if needs_update:
changed = True
else:
# Actually create/update the region
result, region_changed = client.ensure_region(region_spec)
results.append({
'name': region_spec['name'],
'id': result['id'],
'status': 'created' if region_changed and not any(r['name'] == region_spec['name'] for r in client.list_regions() if r['id'] != result['id']) else ('updated' if region_changed else 'unchanged'),
'country': result['country'],
'abbreviation': result['abbreviation'],
'enrollment_key': result.get('enrollment_key'),
'details': result
})
if region_changed:
changed = True
else: # state == 'absent'
if module.check_mode:
existing_regions = client.list_regions()
existing = next((r for r in existing_regions if r['name'] == region_spec['name']), None)
if existing:
results.append({
'name': region_spec['name'],
'id': existing['id'],
'status': 'would_delete',
'country': existing['country'],
'abbreviation': existing['abbreviation'],
'enrollment_key': existing.get('enrollment_key'),
'details': existing
})
changed = True
else:
results.append({
'name': region_spec['name'],
'id': None,
'status': 'not_found',
'country': None,
'abbreviation': None,
'enrollment_key': None,
'details': {}
})
else:
# Actually delete the region
region_deleted = client.delete_region(region_spec['name'])
results.append({
'name': region_spec['name'],
'id': None,
'status': 'deleted' if region_deleted else 'not_found',
'country': None,
'abbreviation': None,
'enrollment_key': None,
'details': {}
})
if region_deleted:
changed = True
except XCloudifyAdminAPIError as e:
module.fail_json(
msg=f"Failed to manage region {region_spec['name']}: {str(e)}",
region=region_spec,
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(
msg=f"Unexpected error managing region {region_spec['name']}: {str(e)}",
region=region_spec
)
module.exit_json(
changed=changed,
regions=results
)
except XCloudifyAdminAPIError as e:
module.fail_json(
msg=f"xCloudify Admin API error: {str(e)}",
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(msg=f"Unexpected error: {str(e)}")
if __name__ == '__main__':
main()
@@ -0,0 +1,484 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, xCloudify Team
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: xcloudify_vdc
short_description: Manage Virtual Data Centers in xCloudify (Admin Only)
description:
- Create, update, or delete Virtual Data Centers in xCloudify
- Administrative operation requiring elevated privileges
- Supports idempotent operations with automatic state detection
version_added: "1.0.0"
options:
bearer_token:
description: Bearer token for authentication (admin required)
required: true
type: str
api_url:
description: xCloudify API URL
required: false
default: https://api.xcloudify.tech
type: str
vdcs:
description: List of VDC specifications
required: true
type: list
elements: dict
suboptions:
name:
description: VDC name
required: true
type: str
region_id:
description: Region ID (UUID)
required: false
type: str
region_name:
description: Region name (alternative to region_id)
required: false
type: str
project_id:
description: Project ID (UUID)
required: true
type: str
description:
description: VDC description
required: false
type: str
state:
description: Desired state
required: false
default: present
choices: ['present', 'absent']
type: str
api_timeout:
description: API request timeout in seconds
required: false
default: 30
type: int
debug:
description: Enable debug logging
required: false
default: false
type: bool
author:
- xCloudify Team
'''
EXAMPLES = '''
- name: Create VDCs
xcloudify_vdc:
bearer_token: "{{ admin_token }}"
vdcs:
- name: "Production VDC"
region_name: "US East 1"
project_id: "123e4567-e89b-12d3-a456-426614174000"
description: "Production environment VDC"
- name: "Development VDC"
region_id: "456e7890-e89b-12d3-a456-426614174001"
project_id: "123e4567-e89b-12d3-a456-426614174000"
description: "Development environment VDC"
- name: Remove VDCs
xcloudify_vdc:
bearer_token: "{{ admin_token }}"
vdcs:
- name: "Old VDC"
state: absent
'''
RETURN = '''
vdcs:
description: List of VDC results
returned: always
type: list
elements: dict
contains:
name:
description: VDC name
type: str
id:
description: VDC ID
type: str
status:
description: Operation status (created, updated, unchanged, deleted, not_found)
type: str
region_id:
description: Region ID
type: str
project_id:
description: Project ID
type: str
details:
description: Full VDC details from API
type: dict
changed:
description: Whether any changes were made
returned: always
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
import json
import uuid
class XCloudifyAdminAPIError(Exception):
"""Custom exception for xCloudify Admin API errors"""
def __init__(self, message, status_code=None, response_data=None):
super(XCloudifyAdminAPIError, self).__init__(message)
self.status_code = status_code
self.response_data = response_data
class XCloudifyAdminClient:
"""xCloudify Admin API client for VDC management"""
def __init__(self, module, api_url, bearer_token):
self.module = module
self.api_url = api_url.rstrip('/')
self.bearer_token = bearer_token
self.headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {bearer_token}'
}
self.timeout = getattr(module.params, 'api_timeout', 30)
self.debug = getattr(module.params, 'debug', False)
def _make_request(self, method, endpoint, data=None):
"""Make HTTP request to xCloudify API"""
url = f"{self.api_url}/api/{endpoint}"
if self.debug:
self.module.debug(f"xCloudify Admin API {method} {url}")
if data:
self.module.debug(f"Request data: {json.dumps(data, indent=2)}")
if data:
data = json.dumps(data)
response, info = fetch_url(
self.module,
url,
method=method,
headers=self.headers,
data=data,
timeout=self.timeout
)
if info['status'] >= 400:
error_msg = f"xCloudify Admin API request failed: {info['status']}"
error_data = None
if response:
try:
error_data = json.loads(response.read())
if 'message' in error_data:
error_msg += f" - {error_data['message']}"
except:
pass
raise XCloudifyAdminAPIError(
error_msg,
status_code=info['status'],
response_data=error_data
)
if response:
result = json.loads(response.read())
if self.debug:
self.module.debug(f"Response data: {json.dumps(result, indent=2)}")
# Handle xCloudify API response envelope
if isinstance(result, dict) and 'success' in result:
if result['success']:
return result.get('data')
else:
raise XCloudifyAdminAPIError(
f"xCloudify Admin API error: {result.get('message', 'Unknown error')}",
response_data=result.get('error_details', {})
)
return result
return None
def list_regions(self):
"""List all regions"""
return self._make_request('GET', 'regions') or []
def list_vdcs(self):
"""List all VDCs"""
return self._make_request('GET', 'virtual_data_centers') or []
def resolve_region_id(self, region_name):
"""Resolve region name to region ID"""
regions = self.list_regions()
for region in regions:
if region['name'] == region_name:
return region['id']
raise XCloudifyAdminAPIError(f"Region '{region_name}' not found")
def ensure_vdc(self, vdc_spec):
"""Idempotent VDC management"""
vdcs = self.list_vdcs()
existing = None
for vdc in vdcs:
if vdc['name'] == vdc_spec['name']:
existing = vdc
break
# Resolve region_id if region_name is provided
region_id = vdc_spec.get('region_id')
if not region_id and 'region_name' in vdc_spec:
region_id = self.resolve_region_id(vdc_spec['region_name'])
elif not region_id:
raise XCloudifyAdminAPIError("Either region_id or region_name must be provided")
if not existing:
# Create VDC
vdc_data = {
'name': vdc_spec['name'],
'region_id': region_id,
'project_id': vdc_spec['project_id'],
'description': vdc_spec.get('description', f"VDC {vdc_spec['name']}")
}
result = self._make_request('POST', 'virtual_data_centers', vdc_data)
return result, True # created
else:
# Check if update needed
needs_update = False
update_data = {}
for field in ['description']:
if field in vdc_spec and existing.get(field) != vdc_spec[field]:
needs_update = True
update_data[field] = vdc_spec[field]
# Check if region_id changed
if existing.get('region_id') != region_id:
needs_update = True
update_data['region_id'] = region_id
if needs_update:
result = self._make_request('PUT', f'virtual_data_centers/{existing["id"]}', update_data)
return result, True # updated
return existing, False # no change
def delete_vdc(self, vdc_name):
"""Delete a VDC by name"""
vdcs = self.list_vdcs()
for vdc in vdcs:
if vdc['name'] == vdc_name:
self._make_request('DELETE', f'virtual_data_centers/{vdc["id"]}')
return True
return False # not found
def validate_vdc_spec(vdc_spec):
"""Validate VDC specification"""
errors = []
if not vdc_spec.get('name'):
errors.append("VDC name is required")
if not vdc_spec.get('project_id'):
errors.append("project_id is required")
else:
try:
uuid.UUID(vdc_spec['project_id'])
except ValueError:
errors.append("project_id must be a valid UUID")
# Must have either region_id or region_name
if not vdc_spec.get('region_id') and not vdc_spec.get('region_name'):
errors.append("Either region_id or region_name must be provided")
if vdc_spec.get('region_id'):
try:
uuid.UUID(vdc_spec['region_id'])
except ValueError:
errors.append("region_id must be a valid UUID")
return errors
def main():
module_args = dict(
bearer_token=dict(type='str', required=True, no_log=True),
api_url=dict(type='str', required=False, default='https://api.xcloudify.tech'),
vdcs=dict(type='list', required=True, elements='dict'),
state=dict(type='str', default='present', choices=['present', 'absent']),
api_timeout=dict(type='int', default=30),
debug=dict(type='bool', default=False)
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# Validate VDC specifications
for vdc_spec in module.params['vdcs']:
errors = validate_vdc_spec(vdc_spec)
if errors:
module.fail_json(
msg=f"Invalid VDC specification for '{vdc_spec.get('name', 'unnamed')}'",
errors=errors
)
try:
client = XCloudifyAdminClient(
module,
module.params['api_url'],
module.params['bearer_token']
)
changed = False
results = []
for vdc_spec in module.params['vdcs']:
try:
if module.params['state'] == 'present':
if module.check_mode:
# In check mode, just validate and report what would be done
existing_vdcs = client.list_vdcs()
existing = next((v for v in existing_vdcs if v['name'] == vdc_spec['name']), None)
# Resolve region_id for display
region_id = vdc_spec.get('region_id')
if not region_id and 'region_name' in vdc_spec:
try:
region_id = client.resolve_region_id(vdc_spec['region_name'])
except XCloudifyAdminAPIError:
region_id = f"UNKNOWN({vdc_spec['region_name']})"
if not existing:
results.append({
'name': vdc_spec['name'],
'id': None,
'status': 'would_create',
'region_id': region_id,
'project_id': vdc_spec['project_id'],
'details': {}
})
changed = True
else:
# Check if update would be needed
needs_update = False
for field in ['description']:
if field in vdc_spec and existing.get(field) != vdc_spec[field]:
needs_update = True
break
if existing.get('region_id') != region_id:
needs_update = True
results.append({
'name': vdc_spec['name'],
'id': existing['id'],
'status': 'would_update' if needs_update else 'unchanged',
'region_id': existing['region_id'],
'project_id': existing['project_id'],
'details': existing
})
if needs_update:
changed = True
else:
# Actually create/update the VDC
result, vdc_changed = client.ensure_vdc(vdc_spec)
results.append({
'name': vdc_spec['name'],
'id': result['id'],
'status': 'created' if vdc_changed and not any(v['name'] == vdc_spec['name'] for v in client.list_vdcs() if v['id'] != result['id']) else ('updated' if vdc_changed else 'unchanged'),
'region_id': result['region_id'],
'project_id': result['project_id'],
'details': result
})
if vdc_changed:
changed = True
else: # state == 'absent'
if module.check_mode:
existing_vdcs = client.list_vdcs()
existing = next((v for v in existing_vdcs if v['name'] == vdc_spec['name']), None)
if existing:
results.append({
'name': vdc_spec['name'],
'id': existing['id'],
'status': 'would_delete',
'region_id': existing['region_id'],
'project_id': existing['project_id'],
'details': existing
})
changed = True
else:
results.append({
'name': vdc_spec['name'],
'id': None,
'status': 'not_found',
'region_id': None,
'project_id': None,
'details': {}
})
else:
# Actually delete the VDC
vdc_deleted = client.delete_vdc(vdc_spec['name'])
results.append({
'name': vdc_spec['name'],
'id': None,
'status': 'deleted' if vdc_deleted else 'not_found',
'region_id': None,
'project_id': None,
'details': {}
})
if vdc_deleted:
changed = True
except XCloudifyAdminAPIError as e:
module.fail_json(
msg=f"Failed to manage VDC {vdc_spec['name']}: {str(e)}",
vdc=vdc_spec,
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(
msg=f"Unexpected error managing VDC {vdc_spec['name']}: {str(e)}",
vdc=vdc_spec
)
module.exit_json(
changed=changed,
vdcs=results
)
except XCloudifyAdminAPIError as e:
module.fail_json(
msg=f"xCloudify Admin API error: {str(e)}",
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(msg=f"Unexpected error: {str(e)}")
if __name__ == '__main__':
main()
@@ -0,0 +1,31 @@
---
galaxy_info:
author: xCloudify Team
description: Ansible role for xCloudify administrative operations
company: xCloudify
license: MIT
min_ansible_version: 2.9
platforms:
- name: EL
versions:
- 7
- 8
- 9
- name: Ubuntu
versions:
- 18.04
- 20.04
- 22.04
- name: Debian
versions:
- 10
- 11
galaxy_tags:
- cloud
- administration
- infrastructure
- xcloudify
- regions
- vdc
dependencies: []
@@ -0,0 +1,339 @@
# xCloudify Infrastructure Ansible Role
An Ansible role for managing containerized infrastructure in xCloudify Virtual Data Centers. This role provides idempotent infrastructure-as-code capabilities for deploying and managing Docker containers, storage volumes, and networking.
## Features
- **VDC-Centric**: Simple interface requiring only Virtual Data Center ID
- **Idempotent Operations**: Safe to run multiple times with consistent results
- **Comprehensive Management**: Networks, volumes, containers, and port forwarding
- **Automatic Context Resolution**: Resolves Universe/Project/Region from VDC
- **Bearer Token Authentication**: Secure API authentication
- **Check Mode Support**: Preview changes before applying
- **Error Handling**: Robust error handling with rollback capabilities
## Requirements
- Ansible >= 2.9
- Python >= 3.6
- Access to xCloudify API
- Valid Virtual Data Center ID
## Role Variables
### Required Variables
```yaml
xcloudify_vdc_id: "550e8400-e29b-41d4-a716-446655440000" # Your VDC ID
```
### Optional Variables
```yaml
# API Configuration
xcloudify_api_url: "https://api.xcloudify.tech" # Default API URL
xcloudify_bearer_token: "your-bearer-token" # Can use environment variables
xcloudify_api_timeout: 30 # API request timeout
xcloudify_validate_ssl: true # SSL certificate validation
# Deployment Behavior
xcloudify_wait_for_deployment: true # Wait for containers to start
xcloudify_deployment_timeout: 300 # Deployment timeout in seconds
xcloudify_force_recreate: false # Force container recreation
xcloudify_cleanup_on_failure: true # Cleanup failed deployments
# Resource Management
xcloudify_manage_networks: true # Enable network management
xcloudify_manage_volumes: true # Enable volume management
xcloudify_manage_containers: true # Enable container management
xcloudify_manage_port_forwarding: true # Enable port forwarding
# Debug and Logging
xcloudify_debug: false # Enable debug output
```
### Infrastructure Definition
```yaml
xcloudify_infrastructure:
networks:
- name: "web-network"
vni: 1001
ipv4_cidr: "10.1.0.0/24"
ipv4_gateway: "10.1.0.1"
ipv4_dns_servers: "8.8.8.8,8.8.4.4"
description: "Web tier network"
volumes:
- name: "web-data"
size_gb: 100
type: "local"
description: "Web application data"
- name: "database-storage"
size_gb: 500
type: "local"
description: "Database storage"
containers:
- name: "web-server"
image: "nginx:latest"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 80
external: 8080
use_dns: true
storage:
- volume: "web-data"
mount_point: "/usr/share/nginx/html"
read_only: false
networks: ["web-network"]
env:
NGINX_HOST: "example.com"
NGINX_PORT: "80"
```
## Authentication
The role supports multiple authentication methods:
### Bearer Token (Recommended)
```yaml
# In playbook
xcloudify_bearer_token: "{{ vault_xcloudify_token }}"
# Or via environment variable
export XCLOUDIFY_BEARER_TOKEN="your-token-here"
```
### Ansible Vault
```yaml
# In group_vars/all/vault.yml (encrypted)
vault_xcloudify_token: "your-bearer-token"
# In playbook
xcloudify_bearer_token: "{{ vault_xcloudify_token }}"
```
## Usage Examples
### Simple Web Application
```yaml
- name: Deploy simple web application
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ vault_xcloudify_token }}"
xcloudify_infrastructure:
containers:
- name: "my-web-app"
image: "nginx:alpine"
cpu_shares: 1
mem_limit: 256
ports:
- internal: 80
external: 8080
use_dns: true
```
### Multi-Tier Application
```yaml
- name: Deploy multi-tier application
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ production_vdc }}"
xcloudify_bearer_token: "{{ vault_xcloudify_token }}"
xcloudify_debug: true
xcloudify_infrastructure:
networks:
- name: "app-network"
vni: 2001
ipv4_cidr: "10.2.0.0/24"
ipv4_gateway: "10.2.0.1"
ipv4_dns_servers: "8.8.8.8,8.8.4.4"
- name: "db-network"
vni: 2002
ipv4_cidr: "10.2.1.0/24"
ipv4_gateway: "10.2.1.1"
volumes:
- name: "database-data"
size_gb: 100
type: "local"
- name: "app-logs"
size_gb: 20
type: "local"
- name: "app-config"
size_gb: 5
type: "local"
containers:
- name: "database"
image: "postgres:13"
cpu_shares: 2
mem_limit: 1024
storage:
- volume: "database-data"
mount_point: "/var/lib/postgresql/data"
networks: ["db-network"]
env:
POSTGRES_DB: "myapp"
POSTGRES_USER: "appuser"
POSTGRES_PASSWORD: "{{ vault_db_password }}"
- name: "web-app"
image: "myapp:latest"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 3000
external: 3000
use_dns: true
storage:
- volume: "app-logs"
mount_point: "/app/logs"
- volume: "app-config"
mount_point: "/app/config"
read_only: true
networks: ["app-network", "db-network"]
env:
NODE_ENV: "production"
DB_HOST: "database"
DB_NAME: "myapp"
```
### Container Lifecycle Management
```yaml
# Stop containers
- name: Stop application containers
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ vault_xcloudify_token }}"
xcloudify_infrastructure:
containers:
- name: "web-app"
- name: "database"
xcloudify_container_state: stopped
# Restart containers
- name: Restart application containers
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ vault_xcloudify_token }}"
xcloudify_infrastructure:
containers:
- name: "web-app"
xcloudify_container_state: restarted
# Remove containers
- name: Remove old containers
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ my_vdc_id }}"
xcloudify_bearer_token: "{{ vault_xcloudify_token }}"
xcloudify_infrastructure:
containers:
- name: "old-container"
xcloudify_container_state: absent
```
### Check Mode (Dry Run)
```bash
# Preview changes without applying them
ansible-playbook deploy.yml --check
# See what would be created/updated/deleted
ansible-playbook deploy.yml --check --diff
```
## Infrastructure Specification
### Networks
```yaml
networks:
- name: "network-name" # Required: Network name
vni: 1001 # Required: VXLAN Network Identifier (1-16777215)
ipv4_cidr: "10.1.0.0/24" # Optional: IPv4 CIDR block
ipv4_gateway: "10.1.0.1" # Optional: IPv4 gateway
ipv4_dns_servers: "8.8.8.8" # Optional: DNS servers (comma-separated)
description: "Network desc" # Optional: Description
```
### Volumes
```yaml
volumes:
- name: "volume-name" # Required: Volume name
size_gb: 100 # Required: Size in GB
type: "local" # Optional: Volume type (local, nfs, ceph, iscsi, lvm)
description: "Volume desc" # Optional: Description
boot: false # Optional: Boot volume flag
image_id: "image-uuid" # Optional: Create from image
```
### Containers
```yaml
containers:
- name: "container-name" # Required: Container name
image: "nginx:latest" # Required: Docker image
cpu_shares: 1 # Optional: CPU shares (default: 1)
mem_limit: 256 # Optional: Memory limit in MB (default: 256)
ports: # Optional: Port mappings
- internal: 80 # Required: Internal port
external: 8080 # Required: External port
use_dns: true # Optional: Create DNS record
storage: # Optional: Volume mounts
- volume: "volume-name" # Required: Volume name (must exist)
mount_point: "/data" # Required: Mount point in container
read_only: false # Optional: Read-only mount
networks: ["network-name"] # Optional: Network names (must exist)
env: # Optional: Environment variables
KEY1: "value1"
KEY2: "value2"
```
## Error Handling
The role includes comprehensive error handling:
- **API Errors**: Detailed error messages from xCloudify API
- **Validation Errors**: Pre-flight validation of all specifications
- **Timeout Handling**: Configurable timeouts for all operations
- **Retry Logic**: Automatic retry for transient failures
- **Rollback**: Optional cleanup of failed deployments
## Idempotency
The role ensures idempotent operations:
- **State Detection**: Checks existing resources before creation
- **Diff Calculation**: Only modifies resources that have changed
- **Selective Updates**: Updates only changed fields
- **Container Recreation**: Recreates containers when image/config changes
## Dependencies
None. This role is self-contained.
## License
MIT
## Author Information
xCloudify Team - Infrastructure as Code Solutions
@@ -0,0 +1,35 @@
---
# xCloudify API Configuration
xcloudify_api_url: "https://api.xcloudify.tech"
xcloudify_bearer_token: "" # Optional - can be provided via environment or vault
xcloudify_api_timeout: 30
xcloudify_api_retries: 3
# VDC Configuration
xcloudify_vdc_id: "" # Required - Virtual Data Center ID
# Infrastructure State
xcloudify_infrastructure: {}
# Behavior Configuration
xcloudify_validate_ssl: true
xcloudify_force_recreate: false
xcloudify_cleanup_on_failure: true
xcloudify_wait_for_deployment: true
xcloudify_deployment_timeout: 300
# Debug and Logging
xcloudify_debug: false
xcloudify_log_api_calls: false
# Resource Management
xcloudify_manage_networks: true
xcloudify_manage_volumes: true
xcloudify_manage_containers: true
xcloudify_manage_port_forwarding: true
# Default Resource Settings
xcloudify_default_container_cpu: 1
xcloudify_default_container_memory: 256
xcloudify_default_volume_type: "local"
xcloudify_default_network_encapsulation: "VXLAN"
@@ -0,0 +1,68 @@
---
# Handlers for xCloudify Infrastructure role
- name: restart containers
xcloudify_container:
vdc_id: "{{ xcloudify_vdc_id }}"
bearer_token: "{{ xcloudify_bearer_token | default(omit) }}"
api_url: "{{ xcloudify_api_url }}"
containers: "{{ xcloudify_infrastructure.containers }}"
state: restarted
api_timeout: "{{ xcloudify_api_timeout }}"
debug: "{{ xcloudify_debug }}"
when: xcloudify_infrastructure.containers is defined
- name: cleanup failed deployment
block:
- name: Get failed containers
set_fact:
_failed_containers: "{{ _xcloudify_created_containers | dict2items | selectattr('value.container_status', 'equalto', 'error') | map(attribute='key') | list }}"
- name: Remove failed containers
xcloudify_container:
vdc_id: "{{ xcloudify_vdc_id }}"
bearer_token: "{{ xcloudify_bearer_token | default(omit) }}"
api_url: "{{ xcloudify_api_url }}"
containers:
- name: "{{ item }}"
state: absent
api_timeout: "{{ xcloudify_api_timeout }}"
debug: "{{ xcloudify_debug }}"
loop: "{{ _failed_containers }}"
when: _failed_containers | length > 0
when:
- xcloudify_cleanup_on_failure | bool
- _xcloudify_created_containers is defined
- name: validate deployment health
uri:
url: "{{ xcloudify_api_url }}/api/workloads/containers/{{ item.value.id }}"
method: GET
headers:
Authorization: "Bearer {{ xcloudify_bearer_token }}"
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _health_check
loop: "{{ _xcloudify_created_containers | dict2items }}"
when:
- item.value.id is defined
- xcloudify_bearer_token is defined and xcloudify_bearer_token != ""
failed_when: _health_check.json.data.status not in ['running', 'stopped']
- name: validate deployment health (no auth)
uri:
url: "{{ xcloudify_api_url }}/api/workloads/containers/{{ item.value.id }}"
method: GET
headers:
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _health_check_noauth
loop: "{{ _xcloudify_created_containers | dict2items }}"
when:
- item.value.id is defined
- xcloudify_bearer_token is not defined or xcloudify_bearer_token == ""
failed_when: _health_check_noauth.json.data.status not in ['running', 'stopped']
@@ -0,0 +1,584 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, xCloudify Team
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: xcloudify_container
short_description: Manage containers in xCloudify Virtual Data Centers
description:
- Create, update, or delete containers in xCloudify Virtual Data Centers
- Supports idempotent operations with automatic state detection
- Manages containerized workloads with networking and storage
version_added: "1.0.0"
options:
vdc_id:
description: Virtual Data Center ID
required: true
type: str
bearer_token:
description: Bearer token for authentication
required: false
type: str
api_url:
description: xCloudify API URL
required: false
default: https://api.xcloudify.tech
type: str
containers:
description: List of container specifications
required: true
type: list
elements: dict
suboptions:
name:
description: Container name
required: true
type: str
image:
description: Docker image
required: true
type: str
cpu_shares:
description: CPU shares allocation
required: false
default: 1
type: int
mem_limit:
description: Memory limit in MB
required: false
default: 256
type: int
ports:
description: Port mappings
required: false
type: list
elements: dict
suboptions:
internal:
description: Internal port
required: true
type: int
external:
description: External port
required: true
type: int
use_dns:
description: Create DNS record
required: false
default: false
type: bool
storage:
description: Volume mounts
required: false
type: list
elements: dict
suboptions:
volume:
description: Volume name
required: true
type: str
mount_point:
description: Mount point in container
required: true
type: str
read_only:
description: Mount as read-only
required: false
default: false
type: bool
networks:
description: Network names to attach
required: false
type: list
elements: str
env:
description: Environment variables
required: false
type: dict
networks:
description: Available networks (resolved from other tasks)
required: false
type: dict
volumes:
description: Available volumes (resolved from other tasks)
required: false
type: dict
state:
description: Desired state
required: false
default: present
choices: ['present', 'absent', 'started', 'stopped', 'restarted']
type: str
wait_for_deployment:
description: Wait for container to reach running state
required: false
default: true
type: bool
deployment_timeout:
description: Deployment timeout in seconds
required: false
default: 300
type: int
api_timeout:
description: API request timeout in seconds
required: false
default: 30
type: int
debug:
description: Enable debug logging
required: false
default: false
type: bool
author:
- xCloudify Team
'''
EXAMPLES = '''
- name: Deploy containers
xcloudify_container:
vdc_id: "550e8400-e29b-41d4-a716-446655440000"
bearer_token: "{{ xcloudify_token }}"
containers:
- name: "web-server"
image: "nginx:latest"
cpu_shares: 2
mem_limit: 512
ports:
- internal: 80
external: 8080
use_dns: true
storage:
- volume: "web-data"
mount_point: "/usr/share/nginx/html"
networks: ["web-network"]
env:
NGINX_HOST: "example.com"
NGINX_PORT: "80"
- name: "database"
image: "postgres:13"
cpu_shares: 2
mem_limit: 1024
storage:
- volume: "db-data"
mount_point: "/var/lib/postgresql/data"
networks: ["db-network"]
env:
POSTGRES_DB: "myapp"
POSTGRES_USER: "appuser"
POSTGRES_PASSWORD: "secret"
- name: Stop containers
xcloudify_container:
vdc_id: "550e8400-e29b-41d4-a716-446655440000"
bearer_token: "{{ xcloudify_token }}"
containers:
- name: "web-server"
state: stopped
- name: Remove containers
xcloudify_container:
vdc_id: "550e8400-e29b-41d4-a716-446655440000"
bearer_token: "{{ xcloudify_token }}"
containers:
- name: "old-container"
state: absent
'''
RETURN = '''
containers:
description: List of container results
returned: always
type: list
elements: dict
contains:
name:
description: Container name
type: str
id:
description: Container ID
type: str
status:
description: Operation status (created, updated, unchanged, deleted, not_found, started, stopped, restarted)
type: str
image:
description: Docker image
type: str
cpu_shares:
description: CPU shares allocation
type: int
mem_limit:
description: Memory limit in MB
type: int
container_status:
description: Current container status
type: str
details:
description: Full container details from API
type: dict
vdc_context:
description: VDC context information
returned: always
type: dict
contains:
vdc:
description: Virtual Data Center details
type: dict
region:
description: Region details
type: dict
project:
description: Project details
type: dict
universe:
description: Universe details
type: dict
changed:
description: Whether any changes were made
returned: always
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.xcloudify_client import XCloudifyClient, XCloudifyAPIError
import time
def validate_container_spec(container_spec):
"""Validate container specification"""
errors = []
if not container_spec.get('name'):
errors.append("Container name is required")
if not container_spec.get('image'):
errors.append("Container image is required")
if 'cpu_shares' in container_spec:
if not isinstance(container_spec['cpu_shares'], int) or container_spec['cpu_shares'] <= 0:
errors.append("cpu_shares must be a positive integer")
if 'mem_limit' in container_spec:
if not isinstance(container_spec['mem_limit'], int) or container_spec['mem_limit'] <= 0:
errors.append("mem_limit must be a positive integer")
if 'ports' in container_spec:
if not isinstance(container_spec['ports'], list):
errors.append("ports must be a list")
else:
for i, port in enumerate(container_spec['ports']):
if not isinstance(port, dict):
errors.append(f"ports[{i}] must be a dictionary")
continue
if 'internal' not in port or 'external' not in port:
errors.append(f"ports[{i}] must have 'internal' and 'external' keys")
continue
if not isinstance(port['internal'], int) or not isinstance(port['external'], int):
errors.append(f"ports[{i}] internal and external must be integers")
if not (1 <= port['internal'] <= 65535) or not (1 <= port['external'] <= 65535):
errors.append(f"ports[{i}] port numbers must be between 1 and 65535")
if 'storage' in container_spec:
if not isinstance(container_spec['storage'], list):
errors.append("storage must be a list")
else:
for i, storage in enumerate(container_spec['storage']):
if not isinstance(storage, dict):
errors.append(f"storage[{i}] must be a dictionary")
continue
if 'volume' not in storage or 'mount_point' not in storage:
errors.append(f"storage[{i}] must have 'volume' and 'mount_point' keys")
continue
if not storage['mount_point'].startswith('/'):
errors.append(f"storage[{i}] mount_point must be an absolute path")
if 'networks' in container_spec:
if not isinstance(container_spec['networks'], list):
errors.append("networks must be a list")
else:
for i, network in enumerate(container_spec['networks']):
if not isinstance(network, str):
errors.append(f"networks[{i}] must be a string")
if 'env' in container_spec:
if not isinstance(container_spec['env'], dict):
errors.append("env must be a dictionary")
else:
for key, value in container_spec['env'].items():
if not isinstance(key, str) or not isinstance(value, str):
errors.append(f"env[{key}] key and value must be strings")
return errors
def main():
module_args = dict(
vdc_id=dict(type='str', required=True),
bearer_token=dict(type='str', required=False, no_log=True),
api_url=dict(type='str', required=False, default='https://api.xcloudify.tech'),
containers=dict(type='list', required=True, elements='dict'),
networks=dict(type='dict', required=False, default={}),
volumes=dict(type='dict', required=False, default={}),
state=dict(type='str', default='present', choices=['present', 'absent', 'started', 'stopped', 'restarted']),
wait_for_deployment=dict(type='bool', default=True),
deployment_timeout=dict(type='int', default=300),
api_timeout=dict(type='int', default=30),
debug=dict(type='bool', default=False)
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# Validate container specifications
for container_spec in module.params['containers']:
errors = validate_container_spec(container_spec)
if errors:
module.fail_json(
msg=f"Invalid container specification for '{container_spec.get('name', 'unnamed')}'",
errors=errors
)
try:
client = XCloudifyClient(
module,
module.params['api_url'],
module.params['bearer_token']
)
# Resolve VDC context
context = client.get_vdc_context(module.params['vdc_id'])
changed = False
results = []
for container_spec in module.params['containers']:
try:
if module.params['state'] == 'present':
if module.check_mode:
# In check mode, just validate and report what would be done
existing_containers = client.list_containers(module.params['vdc_id'])
existing = next((c for c in existing_containers if c['name'] == container_spec['name']), None)
if not existing:
results.append({
'name': container_spec['name'],
'id': None,
'status': 'would_create',
'image': container_spec['image'],
'cpu_shares': container_spec.get('cpu_shares', 1),
'mem_limit': container_spec.get('mem_limit', 256),
'container_status': None,
'details': {}
})
changed = True
else:
# Check if recreation would be needed
needs_recreation = client._container_needs_recreation(existing, container_spec)
results.append({
'name': container_spec['name'],
'id': existing['id'],
'status': 'would_recreate' if needs_recreation else 'unchanged',
'image': existing.get('image', container_spec['image']),
'cpu_shares': existing.get('cpu_shares', container_spec.get('cpu_shares', 1)),
'mem_limit': existing.get('mem_limit', container_spec.get('mem_limit', 256)),
'container_status': existing.get('status'),
'details': existing
})
if needs_recreation:
changed = True
else:
# Actually create/update the container
result, container_changed = client.ensure_container(
container_spec,
module.params['vdc_id'],
module.params['networks'],
module.params['volumes']
)
container_ids = result.get('container_ids', [])
container_id = container_ids[0] if container_ids else None
# Wait for deployment if requested
container_status = 'unknown'
if container_id and module.params['wait_for_deployment'] and container_changed:
if client.wait_for_container_status(container_id, 'running', module.params['deployment_timeout']):
container_status = 'running'
else:
container_status = 'deployment_timeout'
results.append({
'name': container_spec['name'],
'id': container_id,
'status': 'created' if container_changed else 'unchanged',
'image': container_spec['image'],
'cpu_shares': container_spec.get('cpu_shares', 1),
'mem_limit': container_spec.get('mem_limit', 256),
'container_status': container_status,
'details': result
})
if container_changed:
changed = True
elif module.params['state'] in ['started', 'stopped', 'restarted']:
# Lifecycle operations
action = module.params['state'].replace('ed', '') # started -> start, stopped -> stop
if module.params['state'] == 'restarted':
action = 'restart'
if module.check_mode:
existing_containers = client.list_containers(module.params['vdc_id'])
existing = next((c for c in existing_containers if c['name'] == container_spec['name']), None)
if existing:
results.append({
'name': container_spec['name'],
'id': existing['id'],
'status': f'would_{action}',
'image': existing.get('image'),
'cpu_shares': existing.get('cpu_shares'),
'mem_limit': existing.get('mem_limit'),
'container_status': existing.get('status'),
'details': existing
})
changed = True
else:
results.append({
'name': container_spec['name'],
'id': None,
'status': 'not_found',
'image': None,
'cpu_shares': None,
'mem_limit': None,
'container_status': None,
'details': {}
})
else:
# Actually perform lifecycle action
action_performed = client.container_lifecycle(
container_spec['name'],
module.params['vdc_id'],
action
)
if action_performed:
# Get updated container info
containers = client.list_containers(module.params['vdc_id'])
container = next((c for c in containers if c['name'] == container_spec['name']), {})
results.append({
'name': container_spec['name'],
'id': container.get('id'),
'status': f'{action}ed',
'image': container.get('image'),
'cpu_shares': container.get('cpu_shares'),
'mem_limit': container.get('mem_limit'),
'container_status': container.get('status'),
'details': container
})
changed = True
else:
results.append({
'name': container_spec['name'],
'id': None,
'status': 'not_found',
'image': None,
'cpu_shares': None,
'mem_limit': None,
'container_status': None,
'details': {}
})
else: # state == 'absent'
if module.check_mode:
existing_containers = client.list_containers(module.params['vdc_id'])
existing = next((c for c in existing_containers if c['name'] == container_spec['name']), None)
if existing:
results.append({
'name': container_spec['name'],
'id': existing['id'],
'status': 'would_delete',
'image': existing.get('image'),
'cpu_shares': existing.get('cpu_shares'),
'mem_limit': existing.get('mem_limit'),
'container_status': existing.get('status'),
'details': existing
})
changed = True
else:
results.append({
'name': container_spec['name'],
'id': None,
'status': 'not_found',
'image': None,
'cpu_shares': None,
'mem_limit': None,
'container_status': None,
'details': {}
})
else:
# Actually delete the container
container_deleted = client.delete_container(
container_spec['name'],
module.params['vdc_id']
)
results.append({
'name': container_spec['name'],
'id': None,
'status': 'deleted' if container_deleted else 'not_found',
'image': None,
'cpu_shares': None,
'mem_limit': None,
'container_status': None,
'details': {}
})
if container_deleted:
changed = True
except XCloudifyAPIError as e:
module.fail_json(
msg=f"Failed to manage container {container_spec['name']}: {str(e)}",
container=container_spec,
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(
msg=f"Unexpected error managing container {container_spec['name']}: {str(e)}",
container=container_spec
)
module.exit_json(
changed=changed,
containers=results,
vdc_context=context
)
except XCloudifyAPIError as e:
module.fail_json(
msg=f"xCloudify API error: {str(e)}",
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(msg=f"Unexpected error: {str(e)}")
if __name__ == '__main__':
main()
@@ -0,0 +1,357 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, xCloudify Team
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: xcloudify_network
short_description: Manage networks in xCloudify Virtual Data Centers
description:
- Create, update, or delete networks in xCloudify Virtual Data Centers
- Supports idempotent operations with automatic state detection
- Manages VXLAN networks with VNI allocation
version_added: "1.0.0"
options:
vdc_id:
description: Virtual Data Center ID
required: true
type: str
bearer_token:
description: Bearer token for authentication
required: false
type: str
api_url:
description: xCloudify API URL
required: false
default: https://api.xcloudify.tech
type: str
networks:
description: List of network specifications
required: true
type: list
elements: dict
suboptions:
name:
description: Network name
required: true
type: str
vni:
description: VXLAN Network Identifier
required: true
type: int
ipv4_cidr:
description: IPv4 CIDR block
required: false
type: str
ipv4_gateway:
description: IPv4 gateway address
required: false
type: str
ipv4_dns_servers:
description: DNS servers (comma-separated)
required: false
type: str
description:
description: Network description
required: false
type: str
state:
description: Desired state
required: false
default: present
choices: ['present', 'absent']
type: str
api_timeout:
description: API request timeout in seconds
required: false
default: 30
type: int
debug:
description: Enable debug logging
required: false
default: false
type: bool
author:
- xCloudify Team
'''
EXAMPLES = '''
- name: Create networks
xcloudify_network:
vdc_id: "550e8400-e29b-41d4-a716-446655440000"
bearer_token: "{{ xcloudify_token }}"
networks:
- name: "web-network"
vni: 1001
ipv4_cidr: "10.1.0.0/24"
ipv4_gateway: "10.1.0.1"
ipv4_dns_servers: "8.8.8.8,8.8.4.4"
description: "Web tier network"
- name: "db-network"
vni: 1002
ipv4_cidr: "10.2.0.0/24"
ipv4_gateway: "10.2.0.1"
description: "Database tier network"
- name: Remove networks
xcloudify_network:
vdc_id: "550e8400-e29b-41d4-a716-446655440000"
bearer_token: "{{ xcloudify_token }}"
networks:
- name: "old-network"
state: absent
'''
RETURN = '''
networks:
description: List of network results
returned: always
type: list
elements: dict
contains:
name:
description: Network name
type: str
id:
description: Network ID
type: str
status:
description: Operation status (created, updated, unchanged, deleted, not_found)
type: str
vni:
description: VXLAN Network Identifier
type: int
ipv4_cidr:
description: IPv4 CIDR block
type: str
details:
description: Full network details from API
type: dict
vdc_context:
description: VDC context information
returned: always
type: dict
contains:
vdc:
description: Virtual Data Center details
type: dict
region:
description: Region details
type: dict
project:
description: Project details
type: dict
universe:
description: Universe details
type: dict
changed:
description: Whether any changes were made
returned: always
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.xcloudify_client import XCloudifyClient, XCloudifyAPIError
def validate_network_spec(network_spec):
"""Validate network specification"""
errors = []
if not network_spec.get('name'):
errors.append("Network name is required")
if not isinstance(network_spec.get('vni'), int):
errors.append("VNI must be an integer")
elif not (1 <= network_spec['vni'] <= 16777215):
errors.append("VNI must be between 1 and 16777215")
if 'ipv4_cidr' in network_spec:
import ipaddress
try:
ipaddress.ip_network(network_spec['ipv4_cidr'])
except ValueError:
errors.append(f"Invalid IPv4 CIDR: {network_spec['ipv4_cidr']}")
if 'ipv4_gateway' in network_spec:
import ipaddress
try:
ipaddress.ip_address(network_spec['ipv4_gateway'])
except ValueError:
errors.append(f"Invalid IPv4 gateway: {network_spec['ipv4_gateway']}")
return errors
def main():
module_args = dict(
vdc_id=dict(type='str', required=True),
bearer_token=dict(type='str', required=False, no_log=True),
api_url=dict(type='str', required=False, default='https://api.xcloudify.tech'),
networks=dict(type='list', required=True, elements='dict'),
state=dict(type='str', default='present', choices=['present', 'absent']),
api_timeout=dict(type='int', default=30),
debug=dict(type='bool', default=False)
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# Validate network specifications
for network_spec in module.params['networks']:
errors = validate_network_spec(network_spec)
if errors:
module.fail_json(
msg=f"Invalid network specification for '{network_spec.get('name', 'unnamed')}'",
errors=errors
)
try:
client = XCloudifyClient(
module,
module.params['api_url'],
module.params['bearer_token']
)
# Resolve VDC context
context = client.get_vdc_context(module.params['vdc_id'])
changed = False
results = []
for network_spec in module.params['networks']:
try:
if module.params['state'] == 'present':
if module.check_mode:
# In check mode, just validate and report what would be done
existing_networks = client.list_networks(module.params['vdc_id'])
existing = next((n for n in existing_networks if n['name'] == network_spec['name']), None)
if not existing:
results.append({
'name': network_spec['name'],
'id': None,
'status': 'would_create',
'vni': network_spec['vni'],
'ipv4_cidr': network_spec.get('ipv4_cidr'),
'details': {}
})
changed = True
else:
# Check if update would be needed
needs_update = False
for field in ['vni', 'ipv4_cidr', 'ipv4_gateway', 'ipv4_dns_servers', 'description']:
if field in network_spec and existing.get(field) != network_spec[field]:
needs_update = True
break
results.append({
'name': network_spec['name'],
'id': existing['id'],
'status': 'would_update' if needs_update else 'unchanged',
'vni': existing['vni'],
'ipv4_cidr': existing.get('ipv4_cidr'),
'details': existing
})
if needs_update:
changed = True
else:
# Actually create/update the network
result, network_changed = client.ensure_network(
network_spec,
module.params['vdc_id']
)
results.append({
'name': network_spec['name'],
'id': result['id'],
'status': 'created' if network_changed and not any(n['name'] == network_spec['name'] for n in client.list_networks(module.params['vdc_id']) if n['id'] != result['id']) else ('updated' if network_changed else 'unchanged'),
'vni': result['vni'],
'ipv4_cidr': result.get('ipv4_cidr'),
'details': result
})
if network_changed:
changed = True
else: # state == 'absent'
if module.check_mode:
existing_networks = client.list_networks(module.params['vdc_id'])
existing = next((n for n in existing_networks if n['name'] == network_spec['name']), None)
if existing:
results.append({
'name': network_spec['name'],
'id': existing['id'],
'status': 'would_delete',
'vni': existing['vni'],
'ipv4_cidr': existing.get('ipv4_cidr'),
'details': existing
})
changed = True
else:
results.append({
'name': network_spec['name'],
'id': None,
'status': 'not_found',
'vni': None,
'ipv4_cidr': None,
'details': {}
})
else:
# Actually delete the network
network_deleted = client.delete_network(
network_spec['name'],
module.params['vdc_id']
)
results.append({
'name': network_spec['name'],
'id': None,
'status': 'deleted' if network_deleted else 'not_found',
'vni': None,
'ipv4_cidr': None,
'details': {}
})
if network_deleted:
changed = True
except XCloudifyAPIError as e:
module.fail_json(
msg=f"Failed to manage network {network_spec['name']}: {str(e)}",
network=network_spec,
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(
msg=f"Unexpected error managing network {network_spec['name']}: {str(e)}",
network=network_spec
)
module.exit_json(
changed=changed,
networks=results,
vdc_context=context
)
except XCloudifyAPIError as e:
module.fail_json(
msg=f"xCloudify API error: {str(e)}",
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(msg=f"Unexpected error: {str(e)}")
if __name__ == '__main__':
main()
@@ -0,0 +1,393 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, xCloudify Team
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: xcloudify_volume
short_description: Manage volumes in xCloudify Virtual Data Centers
description:
- Create, update, or delete volumes in xCloudify Virtual Data Centers
- Supports idempotent operations with automatic state detection
- Manages persistent storage volumes with various types
version_added: "1.0.0"
options:
vdc_id:
description: Virtual Data Center ID
required: true
type: str
bearer_token:
description: Bearer token for authentication
required: false
type: str
api_url:
description: xCloudify API URL
required: false
default: https://api.xcloudify.tech
type: str
volumes:
description: List of volume specifications
required: true
type: list
elements: dict
suboptions:
name:
description: Volume name
required: true
type: str
size_gb:
description: Volume size in GB
required: true
type: int
type:
description: Volume type
required: false
default: local
choices: ['local', 'nfs', 'ceph', 'iscsi', 'lvm']
type: str
description:
description: Volume description
required: false
type: str
boot:
description: Whether this is a boot volume
required: false
default: false
type: bool
image_id:
description: Image ID to create volume from
required: false
type: str
state:
description: Desired state
required: false
default: present
choices: ['present', 'absent']
type: str
api_timeout:
description: API request timeout in seconds
required: false
default: 30
type: int
debug:
description: Enable debug logging
required: false
default: false
type: bool
author:
- xCloudify Team
'''
EXAMPLES = '''
- name: Create volumes
xcloudify_volume:
vdc_id: "550e8400-e29b-41d4-a716-446655440000"
bearer_token: "{{ xcloudify_token }}"
volumes:
- name: "web-data"
size_gb: 100
type: "local"
description: "Web application data"
- name: "database-storage"
size_gb: 500
type: "local"
description: "Database storage volume"
- name: "boot-volume"
size_gb: 20
type: "local"
boot: true
image_id: "ubuntu-20.04-id"
- name: Remove volumes
xcloudify_volume:
vdc_id: "550e8400-e29b-41d4-a716-446655440000"
bearer_token: "{{ xcloudify_token }}"
volumes:
- name: "old-volume"
state: absent
'''
RETURN = '''
volumes:
description: List of volume results
returned: always
type: list
elements: dict
contains:
name:
description: Volume name
type: str
id:
description: Volume ID
type: str
status:
description: Operation status (created, updated, unchanged, deleted, not_found)
type: str
size_gb:
description: Volume size in GB
type: int
type:
description: Volume type
type: str
boot:
description: Whether this is a boot volume
type: bool
details:
description: Full volume details from API
type: dict
vdc_context:
description: VDC context information
returned: always
type: dict
contains:
vdc:
description: Virtual Data Center details
type: dict
region:
description: Region details
type: dict
project:
description: Project details
type: dict
universe:
description: Universe details
type: dict
changed:
description: Whether any changes were made
returned: always
type: bool
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.xcloudify_client import XCloudifyClient, XCloudifyAPIError
def validate_volume_spec(volume_spec):
"""Validate volume specification"""
errors = []
if not volume_spec.get('name'):
errors.append("Volume name is required")
if not isinstance(volume_spec.get('size_gb'), int):
errors.append("size_gb must be an integer")
elif volume_spec['size_gb'] <= 0:
errors.append("size_gb must be greater than 0")
elif volume_spec['size_gb'] > 10000: # 10TB limit
errors.append("size_gb cannot exceed 10000 GB")
valid_types = ['local', 'nfs', 'ceph', 'iscsi', 'lvm']
if 'type' in volume_spec and volume_spec['type'] not in valid_types:
errors.append(f"type must be one of: {', '.join(valid_types)}")
if 'boot' in volume_spec and not isinstance(volume_spec['boot'], bool):
errors.append("boot must be a boolean")
if 'image_id' in volume_spec:
import uuid
try:
uuid.UUID(volume_spec['image_id'])
except ValueError:
errors.append("image_id must be a valid UUID")
return errors
def main():
module_args = dict(
vdc_id=dict(type='str', required=True),
bearer_token=dict(type='str', required=False, no_log=True),
api_url=dict(type='str', required=False, default='https://api.xcloudify.tech'),
volumes=dict(type='list', required=True, elements='dict'),
state=dict(type='str', default='present', choices=['present', 'absent']),
api_timeout=dict(type='int', default=30),
debug=dict(type='bool', default=False)
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# Validate volume specifications
for volume_spec in module.params['volumes']:
errors = validate_volume_spec(volume_spec)
if errors:
module.fail_json(
msg=f"Invalid volume specification for '{volume_spec.get('name', 'unnamed')}'",
errors=errors
)
try:
client = XCloudifyClient(
module,
module.params['api_url'],
module.params['bearer_token']
)
# Resolve VDC context
context = client.get_vdc_context(module.params['vdc_id'])
changed = False
results = []
for volume_spec in module.params['volumes']:
try:
if module.params['state'] == 'present':
if module.check_mode:
# In check mode, just validate and report what would be done
existing_volumes = client.list_volumes(module.params['vdc_id'])
existing = next((v for v in existing_volumes if v['name'] == volume_spec['name']), None)
if not existing:
results.append({
'name': volume_spec['name'],
'id': None,
'status': 'would_create',
'size_gb': volume_spec['size_gb'],
'type': volume_spec.get('type', 'local'),
'boot': volume_spec.get('boot', False),
'details': {}
})
changed = True
else:
# Check if update would be needed
needs_update = False
for field in ['description', 'type']:
if field in volume_spec and existing.get(field) != volume_spec[field]:
needs_update = True
break
# Check size increase
if existing.get('size_gb', 0) < volume_spec['size_gb']:
needs_update = True
results.append({
'name': volume_spec['name'],
'id': existing['id'],
'status': 'would_update' if needs_update else 'unchanged',
'size_gb': existing['size_gb'],
'type': existing.get('type', 'local'),
'boot': existing.get('boot', False),
'details': existing
})
if needs_update:
changed = True
else:
# Actually create/update the volume
result, volume_changed = client.ensure_volume(
volume_spec,
module.params['vdc_id']
)
# Determine if this was a create or update
existing_volumes = client.list_volumes(module.params['vdc_id'])
was_created = not any(v['name'] == volume_spec['name'] and v['id'] != result['id'] for v in existing_volumes)
results.append({
'name': volume_spec['name'],
'id': result['id'],
'status': 'created' if was_created and volume_changed else ('updated' if volume_changed else 'unchanged'),
'size_gb': result['size_gb'],
'type': result.get('type', 'local'),
'boot': result.get('boot', False),
'details': result
})
if volume_changed:
changed = True
else: # state == 'absent'
if module.check_mode:
existing_volumes = client.list_volumes(module.params['vdc_id'])
existing = next((v for v in existing_volumes if v['name'] == volume_spec['name']), None)
if existing:
results.append({
'name': volume_spec['name'],
'id': existing['id'],
'status': 'would_delete',
'size_gb': existing['size_gb'],
'type': existing.get('type', 'local'),
'boot': existing.get('boot', False),
'details': existing
})
changed = True
else:
results.append({
'name': volume_spec['name'],
'id': None,
'status': 'not_found',
'size_gb': None,
'type': None,
'boot': None,
'details': {}
})
else:
# Actually delete the volume
volume_deleted = client.delete_volume(
volume_spec['name'],
module.params['vdc_id']
)
results.append({
'name': volume_spec['name'],
'id': None,
'status': 'deleted' if volume_deleted else 'not_found',
'size_gb': None,
'type': None,
'boot': None,
'details': {}
})
if volume_deleted:
changed = True
except XCloudifyAPIError as e:
# Check if this is a volume in use error
if hasattr(e, 'response_data') and e.response_data:
error_type = e.response_data.get('error_type')
if error_type == 'VOLUME_IN_USE':
workload_ids = e.response_data.get('error_details', {}).get('workload_ids', [])
module.fail_json(
msg=f"Cannot delete volume {volume_spec['name']}: volume is attached to workloads",
volume=volume_spec,
attached_workloads=workload_ids,
status_code=getattr(e, 'status_code', None)
)
module.fail_json(
msg=f"Failed to manage volume {volume_spec['name']}: {str(e)}",
volume=volume_spec,
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(
msg=f"Unexpected error managing volume {volume_spec['name']}: {str(e)}",
volume=volume_spec
)
module.exit_json(
changed=changed,
volumes=results,
vdc_context=context
)
except XCloudifyAPIError as e:
module.fail_json(
msg=f"xCloudify API error: {str(e)}",
status_code=getattr(e, 'status_code', None),
response_data=getattr(e, 'response_data', None)
)
except Exception as e:
module.fail_json(msg=f"Unexpected error: {str(e)}")
if __name__ == '__main__':
main()
@@ -0,0 +1,30 @@
---
galaxy_info:
author: xCloudify Team
description: Ansible role for managing xCloudify infrastructure
company: xCloudify
license: MIT
min_ansible_version: 2.9
platforms:
- name: EL
versions:
- 7
- 8
- 9
- name: Ubuntu
versions:
- 18.04
- 20.04
- 22.04
- name: Debian
versions:
- 10
- 11
galaxy_tags:
- cloud
- containers
- infrastructure
- xcloudify
- docker
dependencies: []
@@ -0,0 +1,431 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, xCloudify Team
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
import json
import os
import time
import uuid
class XCloudifyAPIError(Exception):
"""Custom exception for xCloudify API errors"""
def __init__(self, message, status_code=None, response_data=None):
super(XCloudifyAPIError, self).__init__(message)
self.status_code = status_code
self.response_data = response_data
class XCloudifyClient:
"""xCloudify API client with idempotent operations"""
def __init__(self, module, api_url=None, bearer_token=None):
self.module = module
self.api_url = (api_url or "https://api.xcloudify.tech").rstrip('/')
# Bearer token priority: parameter > environment variable
self.bearer_token = (
bearer_token or
os.environ.get('XCLOUDIFY_BEARER_TOKEN') or
os.environ.get('XCLOUDIFY_TOKEN')
)
# Set up headers
self.headers = {'Content-Type': 'application/json'}
if self.bearer_token:
self.headers['Authorization'] = f'Bearer {self.bearer_token}'
# Configuration
self.timeout = getattr(module.params, 'api_timeout', 30)
self.retries = getattr(module.params, 'api_retries', 3)
self.debug = getattr(module.params, 'debug', False)
def _make_request(self, method, endpoint, data=None, retry_count=0):
"""Make HTTP request to xCloudify API with retry logic"""
url = f"{self.api_url}/api/{endpoint}"
if self.debug:
self.module.debug(f"xCloudify API {method} {url}")
if data:
self.module.debug(f"Request data: {json.dumps(data, indent=2)}")
if data:
data = json.dumps(data)
try:
response, info = fetch_url(
self.module,
url,
method=method,
headers=self.headers,
data=data,
timeout=self.timeout
)
if self.debug:
self.module.debug(f"Response status: {info['status']}")
# Handle different status codes
if info['status'] >= 500 and retry_count < self.retries:
# Server error - retry
time.sleep(2 ** retry_count) # Exponential backoff
return self._make_request(method, endpoint, data, retry_count + 1)
if info['status'] >= 400:
error_msg = f"xCloudify API request failed: {info['status']}"
error_data = None
if response:
try:
error_data = json.loads(response.read())
if 'message' in error_data:
error_msg += f" - {error_data['message']}"
except:
pass
raise XCloudifyAPIError(
error_msg,
status_code=info['status'],
response_data=error_data
)
if response:
result = json.loads(response.read())
if self.debug:
self.module.debug(f"Response data: {json.dumps(result, indent=2)}")
# Handle xCloudify API response envelope
if isinstance(result, dict) and 'success' in result:
if result['success']:
return result.get('data')
else:
raise XCloudifyAPIError(
f"xCloudify API error: {result.get('message', 'Unknown error')}",
response_data=result.get('error_details', {})
)
return result
return None
except XCloudifyAPIError:
raise
except Exception as e:
if retry_count < self.retries:
time.sleep(2 ** retry_count)
return self._make_request(method, endpoint, data, retry_count + 1)
raise XCloudifyAPIError(f"Request failed: {str(e)}")
def get_vdc_context(self, vdc_id):
"""Get full context for a VDC including region, project, and universe"""
try:
# Validate VDC ID format
uuid.UUID(vdc_id)
except ValueError:
raise XCloudifyAPIError(f"Invalid VDC ID format: {vdc_id}")
vdc = self._make_request('GET', f'virtual_data_centers/{vdc_id}')
if not vdc:
raise XCloudifyAPIError(f"Virtual Data Center {vdc_id} not found")
region = self._make_request('GET', f'regions/{vdc["region_id"]}')
project = self._make_request('GET', f'projects/{vdc["project_id"]}')
universe = self._make_request('GET', f'universes/{project["universe_id"]}')
return {
'vdc': vdc,
'region': region,
'project': project,
'universe': universe
}
def list_networks(self, vdc_id):
"""List all networks in a VDC"""
return self._make_request('GET', f'networks?virtual_datacenter_id={vdc_id}') or []
def ensure_network(self, network_spec, vdc_id):
"""Idempotent network management"""
networks = self.list_networks(vdc_id)
existing = None
for net in networks:
if net['name'] == network_spec['name']:
existing = net
break
if not existing:
# Create network
network_data = {
'name': network_spec['name'],
'vdc_id': vdc_id,
'vni': network_spec['vni'],
'ipv4_cidr': network_spec.get('ipv4_cidr'),
'ipv4_gateway': network_spec.get('ipv4_gateway'),
'ipv4_dns_servers': network_spec.get('ipv4_dns_servers'),
'description': network_spec.get('description', f"Network {network_spec['name']}")
}
result = self._make_request('POST', 'networks', network_data)
return result, True # created
else:
# Check if update needed
needs_update = False
update_data = {}
for field in ['vni', 'ipv4_cidr', 'ipv4_gateway', 'ipv4_dns_servers', 'description']:
if field in network_spec and existing.get(field) != network_spec[field]:
needs_update = True
update_data[field] = network_spec[field]
if needs_update:
result = self._make_request('PUT', f'networks/{existing["id"]}', update_data)
return result, True # updated
return existing, False # no change
def delete_network(self, network_name, vdc_id):
"""Delete a network by name"""
networks = self.list_networks(vdc_id)
for net in networks:
if net['name'] == network_name:
self._make_request('DELETE', f'networks/{net["id"]}')
return True
return False # not found
def list_volumes(self, vdc_id):
"""List all volumes in a VDC"""
result = self._make_request('GET', f'volumes?vdc_id={vdc_id}') or {}
return result.get('volumes', [])
def ensure_volume(self, volume_spec, vdc_id):
"""Idempotent volume management"""
volumes = self.list_volumes(vdc_id)
existing = None
for vol in volumes:
if vol['name'] == volume_spec['name']:
existing = vol
break
if not existing:
# Create volume
volume_data = {
'name': volume_spec['name'],
'size_gb': volume_spec['size_gb'],
'vdc_id': vdc_id,
'type': volume_spec.get('type', 'local'),
'description': volume_spec.get('description', f"Volume {volume_spec['name']}"),
'boot': volume_spec.get('boot', False)
}
if 'image_id' in volume_spec:
volume_data['image_id'] = volume_spec['image_id']
result = self._make_request('POST', 'volumes', volume_data)
return result, True # created
else:
# Check if update needed
needs_update = False
update_data = {}
# Note: Size changes might not be supported by all volume types
for field in ['description', 'type']:
if field in volume_spec and existing.get(field) != volume_spec[field]:
needs_update = True
update_data[field] = volume_spec[field]
# Check size increase (decreases typically not allowed)
if 'size_gb' in volume_spec and existing.get('size_gb', 0) < volume_spec['size_gb']:
needs_update = True
update_data['size_gb'] = volume_spec['size_gb']
if needs_update:
result = self._make_request('PUT', f'volumes/{existing["id"]}', update_data)
return result, True # updated
return existing, False # no change
def delete_volume(self, volume_name, vdc_id):
"""Delete a volume by name"""
volumes = self.list_volumes(vdc_id)
for vol in volumes:
if vol['name'] == volume_name:
self._make_request('DELETE', f'volumes/{vol["id"]}')
return True
return False # not found
def list_containers(self, vdc_id=None):
"""List all containers, optionally filtered by VDC"""
containers = self._make_request('GET', 'workloads/containers') or []
if vdc_id:
return [c for c in containers if c.get('vdc_id') == vdc_id]
return containers
def ensure_container(self, container_spec, vdc_id, networks=None, volumes=None):
"""Idempotent container management"""
networks = networks or {}
volumes = volumes or {}
containers = self.list_containers(vdc_id)
existing = None
for container in containers:
if (container['name'] == container_spec['name'] and
container.get('vdc_id') == vdc_id):
existing = container
break
if not existing:
# Create container
container_data = self._build_container_payload(container_spec, vdc_id, networks, volumes)
result = self._make_request('POST', 'workloads/containers', container_data)
return result, True # created
else:
# For containers, check if recreation is needed
if self._container_needs_recreation(existing, container_spec):
# Delete and recreate
self._make_request('DELETE', f'workloads/containers/{existing["id"]}')
# Wait a moment for cleanup
time.sleep(2)
container_data = self._build_container_payload(container_spec, vdc_id, networks, volumes)
result = self._make_request('POST', 'workloads/containers', container_data)
return result, True # recreated
return existing, False # no change
def delete_container(self, container_name, vdc_id):
"""Delete a container by name"""
containers = self.list_containers(vdc_id)
for container in containers:
if container['name'] == container_name:
self._make_request('DELETE', f'workloads/containers/{container["id"]}')
return True
return False # not found
def container_lifecycle(self, container_name, vdc_id, action):
"""Perform lifecycle action on container (start/stop/restart)"""
containers = self.list_containers(vdc_id)
for container in containers:
if container['name'] == container_name:
self._make_request('POST', f'workloads/containers/{container["id"]}/lifecycle/{action}')
return True
return False # not found
def _build_container_payload(self, container_spec, vdc_id, networks=None, volumes=None):
"""Build container creation payload"""
networks = networks or {}
volumes = volumes or {}
container_data = {
'virtual_data_center': vdc_id,
'containers': [{
'container_name': container_spec['name'],
'docker_image': container_spec['image'],
'cpu_shares': container_spec.get('cpu_shares', 1),
'mem_limit': container_spec.get('mem_limit', 256)
}]
}
container = container_data['containers'][0]
# Add ports
if 'ports' in container_spec:
container['ports'] = []
for port in container_spec['ports']:
port_data = {
'internal': port['internal'],
'external': port['external']
}
if 'use_dns' in port:
port_data['use_dns'] = port['use_dns']
container['ports'].append(port_data)
# Add storage
if 'storage' in container_spec:
container['storage'] = []
for storage in container_spec['storage']:
volume_name = storage['volume']
if volume_name in volumes:
storage_data = {
'volume_id': volumes[volume_name]['id'],
'mount_point': storage['mount_point']
}
if 'read_only' in storage:
storage_data['read_only'] = storage['read_only']
container['storage'].append(storage_data)
# Add networks
if 'networks' in container_spec:
network_names = container_spec['networks']
if network_names:
container['networks'] = network_names
# Add environment variables
if 'env' in container_spec:
if isinstance(container_spec['env'], dict):
container['env'] = container_spec['env']
elif isinstance(container_spec['env'], list):
container['env'] = container_spec['env']
return container_data
def _container_needs_recreation(self, existing, spec):
"""Determine if container needs to be recreated"""
# Parse existing launch params
try:
launch_params = json.loads(existing.get('launch_params', '{}'))
except:
launch_params = {}
# Check key fields that require recreation
if launch_params.get('docker_image') != spec['image']:
return True
if launch_params.get('cpu_shares') != spec.get('cpu_shares', 1):
return True
if launch_params.get('mem_limit') != spec.get('mem_limit', 256):
return True
# Check ports
existing_ports = launch_params.get('ports', [])
spec_ports = spec.get('ports', [])
if len(existing_ports) != len(spec_ports):
return True
# Check environment variables
existing_env = launch_params.get('env', {})
spec_env = spec.get('env', {})
if existing_env != spec_env:
return True
return False
def wait_for_container_status(self, container_id, desired_status, timeout=300):
"""Wait for container to reach desired status"""
start_time = time.time()
while time.time() - start_time < timeout:
try:
container = self._make_request('GET', f'workloads/containers/{container_id}')
if container and container.get('status') == desired_status:
return True
except XCloudifyAPIError:
pass
time.sleep(5)
return False
@@ -0,0 +1,104 @@
---
# Container management tasks
- name: Deploy containers
xcloudify_container:
vdc_id: "{{ xcloudify_vdc_id }}"
bearer_token: "{{ xcloudify_bearer_token | default(omit) }}"
api_url: "{{ xcloudify_api_url }}"
containers: "{{ xcloudify_infrastructure.containers }}"
networks: "{{ _xcloudify_created_networks }}"
volumes: "{{ _xcloudify_created_volumes }}"
state: present
wait_for_deployment: "{{ xcloudify_wait_for_deployment }}"
deployment_timeout: "{{ xcloudify_deployment_timeout }}"
api_timeout: "{{ xcloudify_api_timeout }}"
debug: "{{ xcloudify_debug }}"
register: _container_results
- name: Store container results for reference
set_fact:
_xcloudify_created_containers: "{{ _xcloudify_created_containers | combine({item.name: item}) }}"
loop: "{{ _container_results.containers }}"
when: item.status in ['created', 'updated', 'unchanged']
- name: Display container deployment results
debug:
msg: |
Container Deployment Results:
{% for container in _container_results.containers %}
- {{ container.name }}: {{ container.status }} (ID: {{ container.id | default('N/A') }}, Status: {{ container.container_status | default('N/A') }})
{% endfor %}
when: xcloudify_debug | bool
- name: Check for deployment timeouts
debug:
msg: "Warning: Container {{ item.name }} deployment timed out"
loop: "{{ _container_results.containers }}"
when:
- item.container_status is defined
- item.container_status == 'deployment_timeout'
- name: Fail on container deployment errors
fail:
msg: "Failed to deploy container {{ item.name }}: {{ item.details.message | default('Unknown error') }}"
loop: "{{ _container_results.containers }}"
when: item.status not in ['created', 'updated', 'unchanged', 'would_create', 'would_recreate']
- name: Wait for all containers to be running (if enabled)
uri:
url: "{{ xcloudify_api_url }}/api/workloads/containers/{{ item.id }}"
method: GET
headers:
Authorization: "Bearer {{ xcloudify_bearer_token }}"
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _container_status_check
loop: "{{ _container_results.containers }}"
when:
- xcloudify_wait_for_deployment | bool
- item.id is defined
- item.status == 'created'
- xcloudify_bearer_token is defined and xcloudify_bearer_token != ""
retries: "{{ (xcloudify_deployment_timeout / 10) | int }}"
delay: 10
until: _container_status_check.json.data.status == 'running'
- name: Wait for all containers to be running (no auth)
uri:
url: "{{ xcloudify_api_url }}/api/workloads/containers/{{ item.id }}"
method: GET
headers:
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _container_status_check_noauth
loop: "{{ _container_results.containers }}"
when:
- xcloudify_wait_for_deployment | bool
- item.id is defined
- item.status == 'created'
- xcloudify_bearer_token is not defined or xcloudify_bearer_token == ""
retries: "{{ (xcloudify_deployment_timeout / 10) | int }}"
delay: 10
until: _container_status_check_noauth.json.data.status == 'running'
- name: Update container status after deployment wait
set_fact:
_xcloudify_created_containers: "{{ _xcloudify_created_containers | combine({item.item.name: item.item | combine({'container_status': item.json.data.status})}) }}"
loop: "{{ _container_status_check.results | default([]) + _container_status_check_noauth.results | default([]) }}"
when:
- item.json is defined
- item.json.data is defined
- item.item.name in _xcloudify_created_containers
- name: Display final container status
debug:
msg: |
Final Container Status:
{% for name, container in _xcloudify_created_containers.items() %}
- {{ name }}: {{ container.container_status | default('unknown') }}
{% endfor %}
when: xcloudify_debug | bool
@@ -0,0 +1,46 @@
---
# Main task file for xCloudify Infrastructure role
- name: Validate required variables
include_tasks: validate.yml
- name: Resolve VDC context
include_tasks: validate_vdc.yml
- name: Manage networks
include_tasks: networks.yml
when:
- xcloudify_manage_networks | bool
- xcloudify_infrastructure.networks is defined
- xcloudify_infrastructure.networks | length > 0
- name: Manage volumes
include_tasks: volumes.yml
when:
- xcloudify_manage_volumes | bool
- xcloudify_infrastructure.volumes is defined
- xcloudify_infrastructure.volumes | length > 0
- name: Deploy containers
include_tasks: containers.yml
when:
- xcloudify_manage_containers | bool
- xcloudify_infrastructure.containers is defined
- xcloudify_infrastructure.containers | length > 0
- name: Configure port forwarding
include_tasks: port_forwarding.yml
when:
- xcloudify_manage_port_forwarding | bool
- xcloudify_infrastructure.containers is defined
- xcloudify_infrastructure.containers | selectattr('ports', 'defined') | list | length > 0
- name: Display deployment summary
debug:
msg: |
xCloudify Infrastructure Deployment Summary:
- VDC: {{ _xcloudify_vdc_context.vdc.name }} ({{ _xcloudify_vdc_context.vdc.id }})
- Region: {{ _xcloudify_vdc_context.region.name }}
- Networks: {{ _xcloudify_created_networks.keys() | list | length }}
- Volumes: {{ _xcloudify_created_volumes.keys() | list | length }}
- Containers: {{ _xcloudify_created_containers.keys() | list | length }}
when: xcloudify_debug | bool
@@ -0,0 +1,33 @@
---
# Network management tasks
- name: Manage networks
xcloudify_network:
vdc_id: "{{ xcloudify_vdc_id }}"
bearer_token: "{{ xcloudify_bearer_token | default(omit) }}"
api_url: "{{ xcloudify_api_url }}"
networks: "{{ xcloudify_infrastructure.networks }}"
state: present
api_timeout: "{{ xcloudify_api_timeout }}"
debug: "{{ xcloudify_debug }}"
register: _network_results
- name: Store network results for reference
set_fact:
_xcloudify_created_networks: "{{ _xcloudify_created_networks | combine({item.name: item}) }}"
loop: "{{ _network_results.networks }}"
when: item.status in ['created', 'updated', 'unchanged']
- name: Display network management results
debug:
msg: |
Network Management Results:
{% for network in _network_results.networks %}
- {{ network.name }}: {{ network.status }} (ID: {{ network.id | default('N/A') }})
{% endfor %}
when: xcloudify_debug | bool
- name: Fail on network creation errors
fail:
msg: "Failed to create/update network {{ item.name }}: {{ item.details.message | default('Unknown error') }}"
loop: "{{ _network_results.networks }}"
when: item.status not in ['created', 'updated', 'unchanged', 'would_create', 'would_update']
@@ -0,0 +1,116 @@
---
# Port forwarding and DNS management tasks
- name: Extract containers with port forwarding
set_fact:
_containers_with_ports: "{{ xcloudify_infrastructure.containers | selectattr('ports', 'defined') | list }}"
- name: Display containers requiring port forwarding
debug:
msg: |
Containers with port forwarding:
{% for container in _containers_with_ports %}
- {{ container.name }}: {{ container.ports | length }} port(s)
{% endfor %}
when: xcloudify_debug | bool
- name: Get pod information for containers with DNS
uri:
url: "{{ xcloudify_api_url }}/api/workloads/pods"
method: GET
headers:
Authorization: "Bearer {{ xcloudify_bearer_token }}"
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _pods_response
when:
- _containers_with_ports | length > 0
- xcloudify_bearer_token is defined and xcloudify_bearer_token != ""
- name: Get pod information for containers with DNS (no auth)
uri:
url: "{{ xcloudify_api_url }}/api/workloads/pods"
method: GET
headers:
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _pods_response
when:
- _containers_with_ports | length > 0
- xcloudify_bearer_token is not defined or xcloudify_bearer_token == ""
- name: Extract port forwarding information
set_fact:
_xcloudify_port_forwards: "{{ _xcloudify_port_forwards | combine({item.0.name: {'container_id': (_xcloudify_created_containers[item.0.name].id | default('')), 'ports': item.0.ports}}) }}"
loop: "{{ _containers_with_ports | subelements('ports', skip_missing=True) }}"
when:
- _containers_with_ports | length > 0
- item.0.name in _xcloudify_created_containers
- name: Find pods for our containers
set_fact:
_container_pods: >-
{{
_pods_response.json.data | default([]) |
selectattr('containers', 'defined') |
map('extract', ['containers']) |
flatten |
selectattr('container_name', 'in', _xcloudify_created_containers.keys()) |
list
}}
when:
- _pods_response is defined
- _pods_response.json is defined
- _pods_response.json.data is defined
- name: Display port forwarding status
debug:
msg: |
Port Forwarding Status:
{% for pod in _pods_response.json.data | default([]) %}
{% if pod.port_forwardings is defined and pod.port_forwardings | length > 0 %}
Pod {{ pod.pod_name }}:
{% for pf in pod.port_forwardings %}
- {{ pf.container_name | default('unknown') }}: {{ pf.internal_port }} -> {{ pf.external_port }} ({{ pf.protocol | default('tcp') }})
{% if pf.dns_record_hostname is defined %}
DNS: {{ pf.dns_record_hostname }}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
when:
- xcloudify_debug | bool
- _pods_response is defined
- _pods_response.json is defined
- name: Verify DNS records for containers with use_dns
debug:
msg: |
DNS Records Status:
{% for pod in _pods_response.json.data | default([]) %}
{% for pf in pod.port_forwardings | default([]) %}
{% if pf.dns_record_hostname is defined %}
- {{ pf.container_name | default('unknown') }}: {{ pf.dns_record_hostname }}
{% endif %}
{% endfor %}
{% endfor %}
when:
- xcloudify_debug | bool
- _pods_response is defined
- _pods_response.json is defined
- name: Store port forwarding results
set_fact:
_xcloudify_port_forwarding_summary: >-
{{
_pods_response.json.data | default([]) |
map(attribute='port_forwardings') |
flatten |
list
}}
when:
- _pods_response is defined
- _pods_response.json is defined
@@ -0,0 +1,93 @@
---
# Validation tasks for xCloudify Infrastructure role
- name: Validate xCloudify VDC ID is provided
fail:
msg: "xcloudify_vdc_id is required"
when: not xcloudify_vdc_id or xcloudify_vdc_id == ""
- name: Validate VDC ID format
fail:
msg: "xcloudify_vdc_id must be a valid UUID format"
when: xcloudify_vdc_id is not match("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
- name: Check for bearer token
debug:
msg: "No bearer token provided - will attempt to use environment variables"
when:
- not xcloudify_bearer_token or xcloudify_bearer_token == ""
- xcloudify_debug | bool
- name: Validate API URL format
fail:
msg: "xcloudify_api_url must be a valid URL"
when: xcloudify_api_url is not match("^https?://.*")
- name: Validate infrastructure configuration
fail:
msg: "xcloudify_infrastructure must be defined and contain at least one resource type"
when:
- xcloudify_infrastructure is not defined or xcloudify_infrastructure == {}
- not (xcloudify_infrastructure.networks is defined or
xcloudify_infrastructure.volumes is defined or
xcloudify_infrastructure.containers is defined)
- name: Validate network specifications
fail:
msg: "Network '{{ item.name }}' is missing required fields (name, vni)"
loop: "{{ xcloudify_infrastructure.networks | default([]) }}"
when:
- item.name is not defined or item.name == ""
- item.vni is not defined or not (item.vni | int)
- name: Validate volume specifications
fail:
msg: "Volume '{{ item.name }}' is missing required fields (name, size_gb)"
loop: "{{ xcloudify_infrastructure.volumes | default([]) }}"
when:
- item.name is not defined or item.name == ""
- item.size_gb is not defined or not (item.size_gb | int)
- name: Validate container specifications
fail:
msg: "Container '{{ item.name }}' is missing required fields (name, image)"
loop: "{{ xcloudify_infrastructure.containers | default([]) }}"
when:
- item.name is not defined or item.name == ""
- item.image is not defined or item.image == ""
- name: Validate container storage references
fail:
msg: "Container '{{ item.0.name }}' references undefined volume '{{ item.1.volume }}'"
loop: "{{ xcloudify_infrastructure.containers | default([]) | subelements('storage', skip_missing=True) }}"
when:
- xcloudify_infrastructure.volumes is defined
- item.1.volume not in (xcloudify_infrastructure.volumes | map(attribute='name') | list)
- name: Validate container network references
fail:
msg: "Container '{{ item.0.name }}' references undefined network '{{ item.1 }}'"
loop: "{{ xcloudify_infrastructure.containers | default([]) | subelements('networks', skip_missing=True) }}"
when:
- xcloudify_infrastructure.networks is defined
- item.1 not in (xcloudify_infrastructure.networks | map(attribute='name') | list)
- name: Validate port mappings
fail:
msg: "Container '{{ item.0.name }}' has invalid port mapping: {{ item.1 }}"
loop: "{{ xcloudify_infrastructure.containers | default([]) | subelements('ports', skip_missing=True) }}"
when:
- item.1.internal is not defined or not (item.1.internal | int)
- item.1.external is not defined or not (item.1.external | int)
- (item.1.internal | int) < 1 or (item.1.internal | int) > 65535
- (item.1.external | int) < 1 or (item.1.external | int) > 65535
- name: Display validation summary
debug:
msg: |
Validation Summary:
- VDC ID: {{ xcloudify_vdc_id }}
- API URL: {{ xcloudify_api_url }}
- Networks to manage: {{ xcloudify_infrastructure.networks | default([]) | length }}
- Volumes to manage: {{ xcloudify_infrastructure.volumes | default([]) | length }}
- Containers to deploy: {{ xcloudify_infrastructure.containers | default([]) | length }}
when: xcloudify_debug | bool
@@ -0,0 +1,131 @@
---
# VDC validation and context resolution tasks
- name: Resolve VDC context
uri:
url: "{{ xcloudify_api_url }}/api/virtual_data_centers/{{ xcloudify_vdc_id }}"
method: GET
headers:
Authorization: "Bearer {{ xcloudify_bearer_token }}"
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _vdc_response
when: xcloudify_bearer_token is defined and xcloudify_bearer_token != ""
- name: Resolve VDC context (no auth)
uri:
url: "{{ xcloudify_api_url }}/api/virtual_data_centers/{{ xcloudify_vdc_id }}"
method: GET
headers:
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _vdc_response
when: xcloudify_bearer_token is not defined or xcloudify_bearer_token == ""
- name: Validate VDC response
fail:
msg: "Failed to retrieve VDC information: {{ _vdc_response.json.message | default('Unknown error') }}"
when:
- _vdc_response.json is defined
- not (_vdc_response.json.success | default(false))
- name: Set VDC context
set_fact:
_xcloudify_vdc_context:
vdc: "{{ _vdc_response.json.data }}"
- name: Resolve region information
uri:
url: "{{ xcloudify_api_url }}/api/regions/{{ _xcloudify_vdc_context.vdc.region_id }}"
method: GET
headers:
Authorization: "Bearer {{ xcloudify_bearer_token }}"
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _region_response
when: xcloudify_bearer_token is defined and xcloudify_bearer_token != ""
- name: Resolve region information (no auth)
uri:
url: "{{ xcloudify_api_url }}/api/regions/{{ _xcloudify_vdc_context.vdc.region_id }}"
method: GET
headers:
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _region_response
when: xcloudify_bearer_token is not defined or xcloudify_bearer_token == ""
- name: Resolve project information
uri:
url: "{{ xcloudify_api_url }}/api/projects/{{ _xcloudify_vdc_context.vdc.project_id }}"
method: GET
headers:
Authorization: "Bearer {{ xcloudify_bearer_token }}"
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _project_response
when: xcloudify_bearer_token is defined and xcloudify_bearer_token != ""
- name: Resolve project information (no auth)
uri:
url: "{{ xcloudify_api_url }}/api/projects/{{ _xcloudify_vdc_context.vdc.project_id }}"
method: GET
headers:
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _project_response
when: xcloudify_bearer_token is not defined or xcloudify_bearer_token == ""
- name: Resolve universe information
uri:
url: "{{ xcloudify_api_url }}/api/universes/{{ _project_response.json.data.universe_id }}"
method: GET
headers:
Authorization: "Bearer {{ xcloudify_bearer_token }}"
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _universe_response
when: xcloudify_bearer_token is defined and xcloudify_bearer_token != ""
- name: Resolve universe information (no auth)
uri:
url: "{{ xcloudify_api_url }}/api/universes/{{ _project_response.json.data.universe_id }}"
method: GET
headers:
Content-Type: "application/json"
status_code: 200
timeout: "{{ xcloudify_api_timeout }}"
validate_certs: "{{ xcloudify_validate_ssl }}"
register: _universe_response
when: xcloudify_bearer_token is not defined or xcloudify_bearer_token == ""
- name: Update VDC context with full hierarchy
set_fact:
_xcloudify_vdc_context:
vdc: "{{ _vdc_response.json.data }}"
region: "{{ _region_response.json.data }}"
project: "{{ _project_response.json.data }}"
universe: "{{ _universe_response.json.data }}"
- name: Display resolved context
debug:
msg: |
Resolved xCloudify Context:
- Universe: {{ _xcloudify_vdc_context.universe.name }} ({{ _xcloudify_vdc_context.universe.id }})
- Project: {{ _xcloudify_vdc_context.project.name }} ({{ _xcloudify_vdc_context.project.id }})
- Region: {{ _xcloudify_vdc_context.region.name }} ({{ _xcloudify_vdc_context.region.id }})
- VDC: {{ _xcloudify_vdc_context.vdc.name }} ({{ _xcloudify_vdc_context.vdc.id }})
when: xcloudify_debug | bool
@@ -0,0 +1,33 @@
---
# Volume management tasks
- name: Manage volumes
xcloudify_volume:
vdc_id: "{{ xcloudify_vdc_id }}"
bearer_token: "{{ xcloudify_bearer_token | default(omit) }}"
api_url: "{{ xcloudify_api_url }}"
volumes: "{{ xcloudify_infrastructure.volumes }}"
state: present
api_timeout: "{{ xcloudify_api_timeout }}"
debug: "{{ xcloudify_debug }}"
register: _volume_results
- name: Store volume results for reference
set_fact:
_xcloudify_created_volumes: "{{ _xcloudify_created_volumes | combine({item.name: item}) }}"
loop: "{{ _volume_results.volumes }}"
when: item.status in ['created', 'updated', 'unchanged']
- name: Display volume management results
debug:
msg: |
Volume Management Results:
{% for volume in _volume_results.volumes %}
- {{ volume.name }}: {{ volume.status }} (ID: {{ volume.id | default('N/A') }}, Size: {{ volume.size_gb | default('N/A') }}GB)
{% endfor %}
when: xcloudify_debug | bool
- name: Fail on volume creation errors
fail:
msg: "Failed to create/update volume {{ item.name }}: {{ item.details.message | default('Unknown error') }}"
loop: "{{ _volume_results.volumes }}"
when: item.status not in ['created', 'updated', 'unchanged', 'would_create', 'would_update']
@@ -0,0 +1,36 @@
---
# Internal variables - do not override
_xcloudify_vdc_context: {}
_xcloudify_created_networks: {}
_xcloudify_created_volumes: {}
_xcloudify_created_containers: {}
_xcloudify_port_forwards: {}
# API endpoint mappings
_xcloudify_api_endpoints:
vdc: "virtual_data_centers"
regions: "regions"
projects: "projects"
universes: "universes"
networks: "networks"
volumes: "volumes"
containers: "workloads/containers"
pods: "workloads/pods"
port_forwarding: "port_forwarding"
# Valid resource states
_xcloudify_valid_states:
- present
- absent
- started
- stopped
- restarted
# Container status mappings
_xcloudify_container_states:
running: "running"
stopped: "stopped"
dead: "dead"
deleted: "deleted"
pending: "pending-allocation"
error: "error"
+233
View File
@@ -0,0 +1,233 @@
---
# Integration tests for xCloudify Infrastructure role
- name: Test xCloudify Infrastructure Role
hosts: localhost
gather_facts: false
vars:
test_vdc_id: "{{ lookup('env', 'TEST_VDC_ID') | default('550e8400-e29b-41d4-a716-446655440000') }}"
test_token: "{{ lookup('env', 'XCLOUDIFY_BEARER_TOKEN') }}"
test_prefix: "ansible-test-{{ ansible_date_time.epoch }}"
tasks:
- name: Validate test environment
fail:
msg: "TEST_VDC_ID and XCLOUDIFY_BEARER_TOKEN environment variables must be set"
when: not test_vdc_id or not test_token
# Test 1: Basic infrastructure deployment
- name: Test basic infrastructure deployment
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ test_vdc_id }}"
xcloudify_bearer_token: "{{ test_token }}"
xcloudify_debug: true
xcloudify_infrastructure:
networks:
- name: "{{ test_prefix }}-network"
vni: 9001
ipv4_cidr: "10.9.0.0/24"
ipv4_gateway: "10.9.0.1"
description: "Test network"
volumes:
- name: "{{ test_prefix }}-volume"
size_gb: 10
type: "local"
description: "Test volume"
containers:
- name: "{{ test_prefix }}-container"
image: "nginx:alpine"
cpu_shares: 1
mem_limit: 128
ports:
- internal: 80
external: 8080
storage:
- volume: "{{ test_prefix }}-volume"
mount_point: "/usr/share/nginx/html"
networks: ["{{ test_prefix }}-network"]
- name: Verify infrastructure was created
assert:
that:
- _xcloudify_created_networks is defined
- _xcloudify_created_volumes is defined
- _xcloudify_created_containers is defined
- "test_prefix + '-network' in _xcloudify_created_networks"
- "test_prefix + '-volume' in _xcloudify_created_volumes"
- "test_prefix + '-container' in _xcloudify_created_containers"
fail_msg: "Infrastructure was not created properly"
# Test 2: Idempotency test - run same deployment again
- name: Test idempotency - redeploy same infrastructure
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ test_vdc_id }}"
xcloudify_bearer_token: "{{ test_token }}"
xcloudify_debug: true
xcloudify_infrastructure:
networks:
- name: "{{ test_prefix }}-network"
vni: 9001
ipv4_cidr: "10.9.0.0/24"
ipv4_gateway: "10.9.0.1"
description: "Test network"
volumes:
- name: "{{ test_prefix }}-volume"
size_gb: 10
type: "local"
description: "Test volume"
containers:
- name: "{{ test_prefix }}-container"
image: "nginx:alpine"
cpu_shares: 1
mem_limit: 128
ports:
- internal: 80
external: 8080
storage:
- volume: "{{ test_prefix }}-volume"
mount_point: "/usr/share/nginx/html"
networks: ["{{ test_prefix }}-network"]
# Test 3: Container lifecycle operations
- name: Test container stop
xcloudify_container:
vdc_id: "{{ test_vdc_id }}"
bearer_token: "{{ test_token }}"
containers:
- name: "{{ test_prefix }}-container"
state: stopped
- name: Test container start
xcloudify_container:
vdc_id: "{{ test_vdc_id }}"
bearer_token: "{{ test_token }}"
containers:
- name: "{{ test_prefix }}-container"
state: started
- name: Test container restart
xcloudify_container:
vdc_id: "{{ test_vdc_id }}"
bearer_token: "{{ test_token }}"
containers:
- name: "{{ test_prefix }}-container"
state: restarted
# Test 4: Update operations
- name: Test volume size increase
xcloudify_volume:
vdc_id: "{{ test_vdc_id }}"
bearer_token: "{{ test_token }}"
volumes:
- name: "{{ test_prefix }}-volume"
size_gb: 20 # Increased from 10
type: "local"
description: "Test volume - updated"
- name: Test container image update (recreation)
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ test_vdc_id }}"
xcloudify_bearer_token: "{{ test_token }}"
xcloudify_debug: true
xcloudify_infrastructure:
containers:
- name: "{{ test_prefix }}-container"
image: "nginx:latest" # Changed from nginx:alpine
cpu_shares: 2 # Increased from 1
mem_limit: 256 # Increased from 128
ports:
- internal: 80
external: 8080
storage:
- volume: "{{ test_prefix }}-volume"
mount_point: "/usr/share/nginx/html"
networks: ["{{ test_prefix }}-network"]
# Test 5: Check mode testing
- name: Test check mode - should not make changes
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "{{ test_vdc_id }}"
xcloudify_bearer_token: "{{ test_token }}"
xcloudify_debug: true
xcloudify_infrastructure:
networks:
- name: "{{ test_prefix }}-new-network"
vni: 9002
ipv4_cidr: "10.9.1.0/24"
check_mode: true
# Test 6: Error handling
- name: Test invalid VDC ID handling
include_role:
name: xcloudify_infrastructure
vars:
xcloudify_vdc_id: "invalid-uuid"
xcloudify_bearer_token: "{{ test_token }}"
xcloudify_infrastructure:
containers:
- name: "test-container"
image: "nginx:alpine"
ignore_errors: true
register: invalid_vdc_result
- name: Verify error handling worked
assert:
that:
- invalid_vdc_result is failed
- "'Invalid VDC ID format' in invalid_vdc_result.msg or 'UUID' in invalid_vdc_result.msg"
fail_msg: "Error handling for invalid VDC ID did not work as expected"
# Test 7: Cleanup - remove test resources
- name: Cleanup test containers
xcloudify_container:
vdc_id: "{{ test_vdc_id }}"
bearer_token: "{{ test_token }}"
containers:
- name: "{{ test_prefix }}-container"
state: absent
- name: Cleanup test volumes
xcloudify_volume:
vdc_id: "{{ test_vdc_id }}"
bearer_token: "{{ test_token }}"
volumes:
- name: "{{ test_prefix }}-volume"
state: absent
- name: Cleanup test networks
xcloudify_network:
vdc_id: "{{ test_vdc_id }}"
bearer_token: "{{ test_token }}"
networks:
- name: "{{ test_prefix }}-network"
state: absent
- name: Test completion summary
debug:
msg: |
🧪 TEST SUITE COMPLETED SUCCESSFULLY! 🧪
✅ Tests Passed:
- Basic infrastructure deployment
- Idempotency verification
- Container lifecycle operations
- Resource updates
- Check mode functionality
- Error handling
- Resource cleanup
📊 Test Statistics:
- Test prefix: {{ test_prefix }}
- VDC ID: {{ test_vdc_id }}
- All resources cleaned up