ansible role
This commit is contained in:
+65
-11
@@ -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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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! 🚀
|
||||
@@ -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!"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user