diff --git a/.gitignore b/.gitignore index d3d12b9..926f506 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file + +# Temporary files +tmp/ +temp/ +.tmp/ \ No newline at end of file diff --git a/.kilocodemodes b/.kilocodemodes new file mode 100644 index 0000000..a24f666 --- /dev/null +++ b/.kilocodemodes @@ -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 diff --git a/ansible/CHANGELOG.md b/ansible/CHANGELOG.md new file mode 100644 index 0000000..af52cf9 --- /dev/null +++ b/ansible/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/ansible/GETTING_STARTED.md b/ansible/GETTING_STARTED.md new file mode 100644 index 0000000..8597abc --- /dev/null +++ b/ansible/GETTING_STARTED.md @@ -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! ๐Ÿš€ \ No newline at end of file diff --git a/ansible/Makefile b/ansible/Makefile new file mode 100644 index 0000000..8c37950 --- /dev/null +++ b/ansible/Makefile @@ -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!" \ No newline at end of file diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..2defced --- /dev/null +++ b/ansible/README.md @@ -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 \ No newline at end of file diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..b2d10a5 --- /dev/null +++ b/ansible/ansible.cfg @@ -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 \ No newline at end of file diff --git a/ansible/examples/lifecycle-management.yml b/ansible/examples/lifecycle-management.yml new file mode 100644 index 0000000..fafc830 --- /dev/null +++ b/ansible/examples/lifecycle-management.yml @@ -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 \ No newline at end of file diff --git a/ansible/examples/multi-tier-app.yml b/ansible/examples/multi-tier-app.yml new file mode 100644 index 0000000..2c3cb8b --- /dev/null +++ b/ansible/examples/multi-tier-app.yml @@ -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 %} \ No newline at end of file diff --git a/ansible/examples/simple-web-app.yml b/ansible/examples/simple-web-app.yml new file mode 100644 index 0000000..5e9c6a6 --- /dev/null +++ b/ansible/examples/simple-web-app.yml @@ -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 %} \ No newline at end of file diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100644 index 0000000..1e6dc6f --- /dev/null +++ b/ansible/requirements.yml @@ -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 \ No newline at end of file diff --git a/ansible/roles/xcloudify_admin/library/xcloudify_region.py b/ansible/roles/xcloudify_admin/library/xcloudify_region.py new file mode 100644 index 0000000..87a28be --- /dev/null +++ b/ansible/roles/xcloudify_admin/library/xcloudify_region.py @@ -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() \ No newline at end of file diff --git a/ansible/roles/xcloudify_admin/library/xcloudify_vdc.py b/ansible/roles/xcloudify_admin/library/xcloudify_vdc.py new file mode 100644 index 0000000..8a4b579 --- /dev/null +++ b/ansible/roles/xcloudify_admin/library/xcloudify_vdc.py @@ -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() \ No newline at end of file diff --git a/ansible/roles/xcloudify_admin/meta/main.yml b/ansible/roles/xcloudify_admin/meta/main.yml new file mode 100644 index 0000000..605d64f --- /dev/null +++ b/ansible/roles/xcloudify_admin/meta/main.yml @@ -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: [] \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/README.md b/ansible/roles/xcloudify_infrastructure/README.md new file mode 100644 index 0000000..911040c --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/README.md @@ -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 \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/defaults/main.yml b/ansible/roles/xcloudify_infrastructure/defaults/main.yml new file mode 100644 index 0000000..e9d317d --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/defaults/main.yml @@ -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" \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/handlers/main.yml b/ansible/roles/xcloudify_infrastructure/handlers/main.yml new file mode 100644 index 0000000..19cf3cc --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/handlers/main.yml @@ -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'] \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/library/xcloudify_container.py b/ansible/roles/xcloudify_infrastructure/library/xcloudify_container.py new file mode 100644 index 0000000..143337d --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/library/xcloudify_container.py @@ -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() \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/library/xcloudify_network.py b/ansible/roles/xcloudify_infrastructure/library/xcloudify_network.py new file mode 100644 index 0000000..4d72b6a --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/library/xcloudify_network.py @@ -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() \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/library/xcloudify_volume.py b/ansible/roles/xcloudify_infrastructure/library/xcloudify_volume.py new file mode 100644 index 0000000..9d5bb51 --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/library/xcloudify_volume.py @@ -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() \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/meta/main.yml b/ansible/roles/xcloudify_infrastructure/meta/main.yml new file mode 100644 index 0000000..b065392 --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/meta/main.yml @@ -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: [] \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/module_utils/xcloudify_client.py b/ansible/roles/xcloudify_infrastructure/module_utils/xcloudify_client.py new file mode 100644 index 0000000..fd2eeaf --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/module_utils/xcloudify_client.py @@ -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 \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/tasks/containers.yml b/ansible/roles/xcloudify_infrastructure/tasks/containers.yml new file mode 100644 index 0000000..b1a9d96 --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/tasks/containers.yml @@ -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 \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/tasks/main.yml b/ansible/roles/xcloudify_infrastructure/tasks/main.yml new file mode 100644 index 0000000..2e19e97 --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/tasks/main.yml @@ -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 \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/tasks/networks.yml b/ansible/roles/xcloudify_infrastructure/tasks/networks.yml new file mode 100644 index 0000000..8e1dc4b --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/tasks/networks.yml @@ -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'] \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/tasks/port_forwarding.yml b/ansible/roles/xcloudify_infrastructure/tasks/port_forwarding.yml new file mode 100644 index 0000000..e64e66b --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/tasks/port_forwarding.yml @@ -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 \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/tasks/validate.yml b/ansible/roles/xcloudify_infrastructure/tasks/validate.yml new file mode 100644 index 0000000..4aea35a --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/tasks/validate.yml @@ -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 \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/tasks/validate_vdc.yml b/ansible/roles/xcloudify_infrastructure/tasks/validate_vdc.yml new file mode 100644 index 0000000..737f6fe --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/tasks/validate_vdc.yml @@ -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 \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/tasks/volumes.yml b/ansible/roles/xcloudify_infrastructure/tasks/volumes.yml new file mode 100644 index 0000000..b3606ec --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/tasks/volumes.yml @@ -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'] \ No newline at end of file diff --git a/ansible/roles/xcloudify_infrastructure/vars/main.yml b/ansible/roles/xcloudify_infrastructure/vars/main.yml new file mode 100644 index 0000000..3bc323e --- /dev/null +++ b/ansible/roles/xcloudify_infrastructure/vars/main.yml @@ -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" \ No newline at end of file diff --git a/ansible/tests/test-infrastructure.yml b/ansible/tests/test-infrastructure.yml new file mode 100644 index 0000000..f55f10b --- /dev/null +++ b/ansible/tests/test-infrastructure.yml @@ -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 \ No newline at end of file