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 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 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()