Add per-node random regions and AWS parallel provisioning to WEBRUNNER

Each node chunk now carries its own region field, assigned round-robin
across the full provider region list unless the user pinned a specific
region. AWS uses the same async/poll:0 pattern as Linode but with a
serial per-unique-region setup phase (key import + SG) before firing all
creates in parallel. Teardown uses hostvars ec2_region per node so
multi-region AWS teardowns hit the right endpoint. Infisical replaces
interactive credential prompts for both providers. Linode vars.yaml
instance type corrected to g6-nanode-1.
This commit is contained in:
n0mad1k
2026-04-30 23:48:36 -04:00
parent 9d5bd61a26
commit 2f948c16b1
4 changed files with 246 additions and 104 deletions
+63 -50
View File
@@ -3,73 +3,86 @@
AWS provider utilities for C2ingRed deployment system
"""
import random
import os
import subprocess
import logging
from .common import COLORS, load_vars_file, confirm_action
from .common import COLORS, load_vars_file
def get_aws_credentials(provider_vars=None):
"""Get AWS credentials from user or vars file"""
if not provider_vars:
provider_vars = load_vars_file('aws')
default_aws_key = provider_vars.get('aws_access_key', '')
default_aws_secret = provider_vars.get('aws_secret_key', '')
print(f"\n{COLORS['BLUE']}AWS Configuration{COLORS['RESET']}")
aws_key = input(f"AWS Access Key [{'*****' if default_aws_key else 'leave blank to use AWS CLI profile'}]: ") or default_aws_key
aws_secret = input(f"AWS Secret Key [{'*****' if default_aws_secret else 'leave blank to use AWS CLI profile'}]: ") or default_aws_secret
return {
'aws_access_key': aws_key,
'aws_secret_key': aws_secret
}
"""Get AWS credentials — Infisical first, vars.yaml fallback, then prompt."""
key = ''
secret = ''
def select_aws_region(provider_vars=None, component=None):
"""Let the user select an AWS region"""
try:
key = subprocess.check_output(
[os.path.expanduser('~/.local/bin/creds'), 'get', 'AWS_ACCESS_KEY_ID', 'homelab'],
text=True, stderr=subprocess.DEVNULL,
).strip()
secret = subprocess.check_output(
[os.path.expanduser('~/.local/bin/creds'), 'get', 'AWS_SECRET_ACCESS_KEY', 'homelab'],
text=True, stderr=subprocess.DEVNULL,
).strip()
except Exception:
pass
if not key:
if not provider_vars:
provider_vars = load_vars_file('aws')
key = provider_vars.get('aws_access_key', '')
secret = provider_vars.get('aws_secret_key', '')
if key and 'YOUR_AWS' in key:
key = ''
secret = ''
print(f"\n{COLORS['BLUE']}AWS Configuration{COLORS['RESET']}")
if key:
print(f" AWS credentials loaded")
else:
key = input("AWS Access Key: ").strip()
secret = input("AWS Secret Key: ").strip()
return {'aws_access_key': key, 'aws_secret_key': secret}
def select_aws_regions(provider_vars=None) -> list[str]:
"""Return region list — single entry if user specified one, all regions if blank (random per-node)."""
if not provider_vars:
provider_vars = load_vars_file('aws')
regions = provider_vars.get('aws_region_choices', [])
component_str = f" for {component}" if component else ""
if not regions:
print(f"{COLORS['YELLOW']}No regions found for AWS, using us-east-1{COLORS['RESET']}")
return "us-east-1"
print(f"\nAvailable AWS regions{component_str}:")
regions = provider_vars.get('aws_region_choices', ['us-east-1'])
print(f"\nAvailable AWS regions:")
for i, region in enumerate(regions, 1):
print(f" {i}. {region}")
region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ")
print(f" {i:2}. {region}")
region_input = input("\nSelect region (number or leave blank for random per-node): ").strip()
if not region_input:
return random.choice(regions)
return regions
try:
region_choice = int(region_input)
if 1 <= region_choice <= len(regions):
return regions[region_choice - 1]
else:
print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}")
return random.choice(regions)
idx = int(region_input)
if 1 <= idx <= len(regions):
return [regions[idx - 1]]
except ValueError:
print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}")
return random.choice(regions)
pass
print(f"{COLORS['RED']}Invalid input, using random per-node regions{COLORS['RESET']}")
return regions
def gather_aws_config():
"""Gather all AWS-specific configuration"""
provider_vars = load_vars_file('aws')
config = {}
# Get credentials
aws_creds = get_aws_credentials(provider_vars)
config.update(aws_creds)
# Get region
config['aws_region'] = select_aws_region(provider_vars)
# Additional AWS-specific settings
aws_regions = select_aws_regions(provider_vars)
config['aws_regions'] = aws_regions
config['aws_region'] = aws_regions[0]
config['ami_map'] = provider_vars.get('ami_map', {})
config['aws_instance_type'] = provider_vars.get('aws_instance_type', 't3.micro')
config['aws_volume_size'] = provider_vars.get('aws_volume_size', 20)
return config
+27 -39
View File
@@ -4,7 +4,6 @@ Linode provider utilities for C2ingRed deployment system
"""
import os
import random
import logging
import subprocess
from .common import COLORS, load_vars_file
@@ -13,7 +12,6 @@ def get_linode_credentials(provider_vars=None):
"""Get Linode API token — Infisical first, vars.yaml fallback, then prompt."""
default_token = ''
# Try Infisical first
try:
default_token = subprocess.check_output(
[os.path.expanduser('~/.local/bin/creds'), 'get', 'LINODE_TOKEN', 'homelab'],
@@ -22,7 +20,6 @@ def get_linode_credentials(provider_vars=None):
except Exception:
pass
# Fall back to vars.yaml
if not default_token:
if not provider_vars:
provider_vars = load_vars_file('linode')
@@ -37,56 +34,47 @@ def get_linode_credentials(provider_vars=None):
return {'linode_token': token}
def select_linode_region(provider_vars=None, component=None):
"""Let the user select a Linode region"""
def select_linode_regions(provider_vars=None) -> list[str]:
"""Return region list — single entry if user specified one, all regions if blank (random per-node)."""
if not provider_vars:
provider_vars = load_vars_file('linode')
regions = provider_vars.get('region_choices', [])
component_str = f" for {component}" if component else ""
if not regions:
print(f"{COLORS['YELLOW']}No regions found for Linode, using us-east{COLORS['RESET']}")
return "us-east"
print(f"\nAvailable Linode regions{component_str}:")
regions = provider_vars.get('region_choices', ['us-east'])
print(f"\nAvailable Linode regions:")
for i, region in enumerate(regions, 1):
print(f" {i}. {region}")
region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ")
print(f" {i:2}. {region}")
region_input = input("\nSelect region (number or leave blank for random per-node): ").strip()
if not region_input:
# Use random from reliable regions only (first 8 are most reliable)
reliable_regions = regions[:8] if len(regions) >= 8 else regions
return random.choice(reliable_regions)
return regions
try:
region_choice = int(region_input)
if 1 <= region_choice <= len(regions):
return regions[region_choice - 1]
else:
print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}")
return random.choice(regions)
idx = int(region_input)
if 1 <= idx <= len(regions):
return [regions[idx - 1]]
except ValueError:
print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}")
return random.choice(regions)
pass
print(f"{COLORS['RED']}Invalid input, using random per-node regions{COLORS['RESET']}")
return regions
def gather_linode_config():
"""Gather all Linode-specific configuration"""
provider_vars = load_vars_file('linode')
config = {}
# Get credentials
linode_creds = get_linode_credentials(provider_vars)
if not linode_creds:
return None
config.update(linode_creds)
# Get region
config['linode_region'] = select_linode_region(provider_vars)
# Additional Linode-specific settings
linode_regions = select_linode_regions(provider_vars)
config['linode_regions'] = linode_regions
config['linode_region'] = linode_regions[0]
config['linode_instance_type'] = provider_vars.get('linode_instance_type', 'g6-nanode-1')
config['linode_image'] = provider_vars.get('linode_image', 'linode/kali')
config['linode_image'] = provider_vars.get('linode_image', 'linode/debian12')
return config