diff --git a/infra_cli/config.py b/infra_cli/config.py index 59f9fcd..878f218 100644 --- a/infra_cli/config.py +++ b/infra_cli/config.py @@ -12,12 +12,12 @@ class Config: if not os.path.exists(self.path): # Fallback to local config if exists if os.path.exists("config.yaml"): - self.path = "config.yaml" + self.path = os.path.abspath("config.yaml") else: raise FileNotFoundError(f"Config file not found at {self.path}. Please create it based on config.yaml.example") with open(self.path, 'r') as f: - return yaml.safe_vars(f) if hasattr(yaml, 'safe_vars') else yaml.safe_load(f) + return yaml.safe_load(f) def get(self, key, default=None): parts = key.split('.') @@ -28,3 +28,20 @@ class Config: else: return default return val + + def get_node(self, node_name): + """Helper to get proxmox node details by name or default to first if none provided""" + nodes = self.get('proxmox.nodes', {}) + if not nodes: + # Fallback for old single-host config if present + host = self.get('proxmox.host') + if host: + return {"host": host, "pass": self.get('proxmox.password')} + return None + + if not node_name: + # Default to first node found + return next(iter(nodes.values())) + + return nodes.get(node_name) + diff --git a/infra_cli/dns.py b/infra_cli/dns.py index d824ca2..caf87a9 100644 --- a/infra_cli/dns.py +++ b/infra_cli/dns.py @@ -2,10 +2,15 @@ from .ssh import SSHClient class DNSManager: def __init__(self, config): - self.pve_host = config.get('proxmox.host') + # DNS is on la-vmh-11 (dnsmasq_lxc_id) + node = config.get_node('la-vmh-11') + if not node: + raise ValueError("Node 'la-vmh-11' not found in config") + + self.pve_host = node['host'] self.pve_user = config.get('proxmox.user', 'root') self.lxc_id = config.get('proxmox.dnsmasq_lxc_id') - self.ssh_key = config.get('proxmox.ssh_key') + self.ssh_key = config.get('proxmox.ssh_key_path') self.client = SSHClient(self.pve_host, self.pve_user, self.ssh_key) self.hosts_file = "/etc/dnsmasq.d/dynamic-hosts.conf" diff --git a/infra_cli/ingress.py b/infra_cli/ingress.py index bbfbc7a..a5bbaf3 100644 --- a/infra_cli/ingress.py +++ b/infra_cli/ingress.py @@ -4,8 +4,9 @@ class IngressManager: def __init__(self, config): self.host = config.get('haproxy.host') self.user = config.get('haproxy.user', 'root') - self.ssh_key = config.get('haproxy.ssh_key') - self.client = SSHClient(self.host, self.user, self.ssh_key) + self.ssh_key = config.get('haproxy.ssh_key_path') + self.password = config.get('haproxy.password') + self.client = SSHClient(self.host, self.user, self.ssh_key, self.password) def add(self, domain, ip, port, https=False): cmd = f"ingress-manager add {domain} {ip} {port}" diff --git a/infra_cli/main.py b/infra_cli/main.py index f71a9e1..062a7ee 100644 --- a/infra_cli/main.py +++ b/infra_cli/main.py @@ -3,6 +3,8 @@ from .config import Config from .dns import DNSManager from .ingress import IngressManager from .router import RouterManager +from .proxmox import ProxmoxManager +from .samba import SambaManager import sys @click.group() @@ -16,6 +18,61 @@ def cli(ctx, config): click.echo(f"Error: {e}", err=True) sys.exit(1) +@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""" diff --git a/infra_cli/proxmox.py b/infra_cli/proxmox.py new file mode 100644 index 0000000..bd4ada7 --- /dev/null +++ b/infra_cli/proxmox.py @@ -0,0 +1,52 @@ +from .ssh import SSHClient + +class ProxmoxManager: + def __init__(self, config, node_name=None): + node = config.get_node(node_name) + if not node: + raise ValueError(f"Node '{node_name}' not found in config") + + self.node_name = node_name + self.host = node['host'] + self.password = node.get('pass') + self.user = config.get('proxmox.user', 'root') + self.ssh_key = config.get('proxmox.ssh_key_path') + self.client = SSHClient(self.host, self.user, self.ssh_key, self.password) + + def list_lxcs(self): + res = self.client.run("pct list") + return res.stdout + + def list_vms(self): + res = self.client.run("qm list") + return res.stdout + + def status_lxc(self, vmid): + res = self.client.run(f"pct status {vmid}") + return res.stdout + + def create_lxc(self, vmid, template, hostname, ip, gateway, bridge="vmbr0", storage="local-zfs", password=None): + # Professional creation command with sane defaults + cmd = f"pct create {vmid} {template} --hostname {hostname} " \ + f"--net0 name=eth0,bridge={bridge},ip={ip},gw={gateway} " \ + f"--storage {storage} --onboot 1" + + if password: + cmd += f" --password {password}" + + res = self.client.run(cmd) + if res.returncode != 0: + raise RuntimeError(f"Failed to create LXC {vmid}: {res.stderr}") + return res.stdout + + def start_lxc(self, vmid): + return self.client.run(f"pct start {vmid}") + + def stop_lxc(self, vmid): + return self.client.run(f"pct stop {vmid}") + + def delete_lxc(self, vmid): + res = self.client.run(f"pct destroy {vmid}") + if res.returncode != 0: + raise RuntimeError(f"Failed to destroy LXC {vmid}: {res.stderr}") + return res.stdout diff --git a/infra_cli/samba.py b/infra_cli/samba.py new file mode 100644 index 0000000..32313e2 --- /dev/null +++ b/infra_cli/samba.py @@ -0,0 +1,42 @@ +from .ssh import SSHClient + +class SambaManager: + def __init__(self, config): + # Samba is on la-samba-01 (handled via Jump host or direct IP) + # In this infra, we often use pct exec on the host vmh-11 + node = config.get_node('la-vmh-11') + if not node: + raise ValueError("Node 'la-vmh-11' not found in config") + + self.host = node['host'] + self.password = node.get('pass') + self.user = config.get('proxmox.user', 'root') + self.ssh_key = config.get('proxmox.ssh_key_path') + self.client = SSHClient(self.host, self.user, self.ssh_key, self.password) + + # Container ID for la-samba-01 + self.lxc_id = "1113" + + def exec_samba(self, cmd): + # Executes samba-tool inside the LXC + return self.client.run(f"pct exec {self.lxc_id} -- samba-tool {cmd}") + + def list_users(self): + res = self.exec_samba("user list") + return res.stdout + + def add_user(self, username, password): + res = self.exec_samba(f"user create {username} {password}") + if res.returncode != 0: + raise RuntimeError(f"Failed to create user {username}: {res.stderr}") + return res.stdout + + def add_to_group(self, group, username): + res = self.exec_samba(f"group addmembers {group} {username}") + if res.returncode != 0: + raise RuntimeError(f"Failed to add {username} to {group}: {res.stderr}") + return res.stdout + + def list_group_members(self, group): + res = self.exec_samba(f"group listmembers {group}") + return res.stdout diff --git a/tests/test_cli.py b/tests/test_cli.py index d4d1faa..fe7ec86 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -44,6 +44,29 @@ def test_ingress_cli(unique_id): res = run_infra(["ingress", "remove", domain]) assert res.returncode == 0 +def test_samba_cli(unique_id): + username = f"testuser_{unique_id}" + password = "TestPassword123!" + + # List (verify we can connect) + res = run_infra(["samba", "list-users"]) + assert res.returncode == 0 + + # Add User + res = run_infra(["samba", "add-user", username, password]) + assert res.returncode == 0 + assert username in run_infra(["samba", "list-users"]).stdout + + # Add to Group + res = run_infra(["samba", "add-to-group", "xmpp-users", username]) + assert res.returncode == 0 + +def test_proxmox_cli(unique_id): + # List LXCs on a specific node + res = run_infra(["proxmox", "list-lxcs", "--node", "la-vmh-11"]) + assert res.returncode == 0 + assert "la-dnsmasq-01" in res.stdout or "11209" in res.stdout + def test_router_cli(unique_id): name = f"Test-Cli-{unique_id}" section = name.lower().replace("-", "_")