dynamic-infra-tooling/infra_cli/main.py

313 lines
8.5 KiB
Python

import click
from .config import Config
from .dns import DNSManager
from .ingress import IngressManager
from .router import RouterManager
from .proxmox import ProxmoxManager
from .samba import SambaManager
from .cloudflare import CloudflareManager
from .database import DatabaseManager
import sys
@click.group()
@click.option('--config', help='Path to config file')
@click.pass_context
def cli(ctx, config):
"""LoopAware Infrastructure Management CLI"""
try:
ctx.obj = Config(config)
except Exception as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
@cli.group()
def db():
"""Manage PostgreSQL Databases and Users"""
pass
@db.command(name='list-dbs')
@click.pass_obj
def db_list_dbs(config):
mgr = DatabaseManager(config)
click.echo(mgr.list_databases())
@db.command(name='list-users')
@click.pass_obj
def db_list_users(config):
mgr = DatabaseManager(config)
click.echo(mgr.list_users())
@db.command(name='provision')
@click.argument('project_name')
@click.option('--password', help='Database user password')
@click.pass_obj
def db_provision(config, project_name, password):
"""Create a database and user for a project"""
import secrets
import string
db_name = project_name.lower().replace("-", "_")
username = f"{db_name}_user"
pwd = password or ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(16))
mgr = DatabaseManager(config)
click.echo(f"Creating user {username}...")
mgr.create_user(username, pwd)
click.echo(f"Creating database {db_name} owned by {username}...")
mgr.create_database(db_name, owner=username)
click.echo("\nProvisioning Complete:")
click.echo(f" DB Name: {db_name}")
click.echo(f" Username: {username}")
click.echo(f" Password: {pwd}")
@cli.group()
def cloudflare():
"""Manage Cloudflare DNS and DDNS"""
pass
@cloudflare.command(name='list-ddns')
@click.pass_obj
def cf_list_ddns(config):
mgr = CloudflareManager(config)
for domain in mgr.list_domains():
click.echo(domain)
@cloudflare.command(name='add-ddns')
@click.argument('domain')
@click.pass_obj
def cf_add_ddns(config, domain):
mgr = CloudflareManager(config)
if mgr.add_domain(domain):
click.echo(f"Added {domain} to DDNS update list")
else:
click.echo(f"{domain} already in DDNS update list")
@cloudflare.command(name='remove-ddns')
@click.argument('domain')
@click.pass_obj
def cf_remove_ddns(config, domain):
mgr = CloudflareManager(config)
if mgr.remove_domain(domain):
click.echo(f"Removed {domain} from DDNS update list")
else:
click.echo(f"{domain} not found in DDNS update list")
@cloudflare.command(name='update-ddns')
@click.option('--force', is_flag=True, help='Force update even if IP matches')
@click.pass_obj
def cf_update_ddns(config, force):
mgr = CloudflareManager(config)
click.echo("Updating Cloudflare DDNS records...")
click.echo(mgr.update_ddns(force))
@cli.group()
def proxmox():
"""Manage Proxmox VMs and Containers"""
pass
@proxmox.command(name='list-lxcs')
@click.option('--node', help='Proxmox node name')
@click.pass_obj
def proxmox_list_lxcs(config, node):
mgr = ProxmoxManager(config, node)
click.echo(mgr.list_lxcs())
@proxmox.command(name='create-lxc')
@click.argument('vmid')
@click.argument('template')
@click.argument('hostname')
@click.argument('ip')
@click.argument('gateway')
@click.option('--node', help='Proxmox node name')
@click.option('--password', help='Root password for LXC')
@click.pass_obj
def proxmox_create_lxc(config, vmid, template, hostname, ip, gateway, node, password):
mgr = ProxmoxManager(config, node)
mgr.create_lxc(vmid, template, hostname, ip, gateway, password=password)
click.echo(f"LXC {vmid} ({hostname}) created on {mgr.node_name or 'default node'}")
@cli.group()
def samba():
"""Manage Samba AD Identity"""
pass
@samba.command(name='list-users')
@click.pass_obj
def samba_list_users(config):
mgr = SambaManager(config)
click.echo(mgr.list_users())
@samba.command(name='add-user')
@click.argument('username')
@click.argument('password')
@click.pass_obj
def samba_add_user(config, username, password):
mgr = SambaManager(config)
mgr.add_user(username, password)
click.echo(f"User {username} created")
@samba.command(name='add-to-group')
@click.argument('group')
@click.argument('username')
@click.pass_obj
def samba_add_to_group(config, group, username):
mgr = SambaManager(config)
mgr.add_to_group(group, username)
click.echo(f"User {username} added to group {group}")
@cli.group()
def dns():
"""Manage DNS and DHCP"""
pass
@dns.command(name='add-host')
@click.argument('mac')
@click.argument('ip')
@click.argument('hostname')
@click.pass_obj
def dns_add_host(config, mac, ip, hostname):
mgr = DNSManager(config)
mgr.add_host(mac, ip, hostname)
click.echo(f"Added host {hostname} ({ip})")
@dns.command(name='remove-host')
@click.argument('mac')
@click.pass_obj
def dns_remove_host(config, mac):
mgr = DNSManager(config)
mgr.remove_host(mac)
click.echo(f"Removed host {mac}")
@dns.command(name='add-dns')
@click.argument('domain')
@click.argument('ip')
@click.pass_obj
def dns_add_dns(config, domain, ip):
mgr = DNSManager(config)
mgr.add_dns(domain, ip)
click.echo(f"Added DNS record for {domain} -> {ip}")
@dns.command(name='remove-dns')
@click.argument('domain')
@click.pass_obj
def dns_remove_dns(config, domain):
mgr = DNSManager(config)
mgr.remove_dns(domain)
click.echo(f"Removed DNS record for {domain}")
@dns.command(name='list')
@click.pass_obj
def dns_list(config):
mgr = DNSManager(config)
data = mgr.list()
click.echo(data['hosts'])
click.echo(data['dns'])
@cli.group()
def ip():
"""Manage and discover available IP addresses"""
pass
@ip.command(name='list-free')
@click.option('--count', default=5, help='Number of free IPs to show')
@click.pass_obj
def ip_list_free(config, count):
mgr = DNSManager(config)
free = mgr.get_free_ips()
for ip in free[:count]:
click.echo(ip)
@ip.command(name='next-free')
@click.pass_obj
def ip_next_free(config):
mgr = DNSManager(config)
free = mgr.get_free_ips()
if free:
click.echo(free[0])
else:
click.echo("Error: No free IPs in agent pool!", err=True)
sys.exit(1)
@cli.group()
def ingress():
"""Manage HAProxy Ingress"""
pass
@ingress.command(name='add')
@click.argument('domain')
@click.argument('ip')
@click.argument('port', type=int)
@click.option('--https', is_flag=True, help='Target uses HTTPS')
@click.pass_obj
def ingress_add(config, domain, ip, port, https):
mgr = IngressManager(config)
mgr.add(domain, ip, port, https)
click.echo(f"Added ingress for {domain}")
@ingress.command(name='remove')
@click.argument('domain')
@click.pass_obj
def ingress_remove(config, domain):
mgr = IngressManager(config)
mgr.remove(domain)
click.echo(f"Removed ingress for {domain}")
@ingress.command(name='list')
@click.pass_obj
def ingress_list(config):
mgr = IngressManager(config)
click.echo(mgr.list())
@cli.group()
def router():
"""Manage Router Port Forwards"""
pass
@router.command(name='add')
@click.argument('name')
@click.argument('proto')
@click.argument('ext_port')
@click.argument('int_ip')
@click.argument('int_port')
@click.pass_obj
def router_add(config, name, proto, ext_port, int_ip, int_port):
import ipaddress
# Validate IP and Ports in CLI layer for better error messages
try:
ipaddress.ip_address(int_ip)
except ValueError:
raise click.BadParameter(f"Invalid internal IP address: {int_ip}")
for p in [ext_port, int_port]:
if not (1 <= int(p) <= 65535):
raise click.BadParameter(f"Port {p} out of range (1-65535)")
mgr = RouterManager(config)
mgr.add_forward(name, proto, ext_port, int_ip, int_port)
click.echo(f"Added port forward {name}")
@router.command(name='remove')
@click.argument('section')
@click.pass_obj
def router_remove(config, section):
mgr = RouterManager(config)
try:
mgr.remove_forward(section)
click.echo(f"Removed port forward {section}")
except ValueError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
@router.command(name='list')
@click.pass_obj
def router_list(config):
mgr = RouterManager(config)
for rule in mgr.list():
click.echo(f"[{rule['section']}] {rule['name']}: {rule['proto']} {rule['port']} -> {rule['dest']}")
def main():
cli(obj={})
if __name__ == "__main__":
main()