feat: add samba and proxmox modules, update config with node secrets

This commit is contained in:
Fredrick Amnehagen 2026-02-05 17:13:40 +01:00
parent a80be5d8aa
commit 9c8c771cb1
7 changed files with 203 additions and 6 deletions

View file

@ -12,12 +12,12 @@ class Config:
if not os.path.exists(self.path): if not os.path.exists(self.path):
# Fallback to local config if exists # Fallback to local config if exists
if os.path.exists("config.yaml"): if os.path.exists("config.yaml"):
self.path = "config.yaml" self.path = os.path.abspath("config.yaml")
else: else:
raise FileNotFoundError(f"Config file not found at {self.path}. Please create it based on config.yaml.example") 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: 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): def get(self, key, default=None):
parts = key.split('.') parts = key.split('.')
@ -28,3 +28,20 @@ class Config:
else: else:
return default return default
return val 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)

View file

@ -2,10 +2,15 @@ from .ssh import SSHClient
class DNSManager: class DNSManager:
def __init__(self, config): 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.pve_user = config.get('proxmox.user', 'root')
self.lxc_id = config.get('proxmox.dnsmasq_lxc_id') 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.client = SSHClient(self.pve_host, self.pve_user, self.ssh_key)
self.hosts_file = "/etc/dnsmasq.d/dynamic-hosts.conf" self.hosts_file = "/etc/dnsmasq.d/dynamic-hosts.conf"

View file

@ -4,8 +4,9 @@ class IngressManager:
def __init__(self, config): def __init__(self, config):
self.host = config.get('haproxy.host') self.host = config.get('haproxy.host')
self.user = config.get('haproxy.user', 'root') self.user = config.get('haproxy.user', 'root')
self.ssh_key = config.get('haproxy.ssh_key') self.ssh_key = config.get('haproxy.ssh_key_path')
self.client = SSHClient(self.host, self.user, self.ssh_key) 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): def add(self, domain, ip, port, https=False):
cmd = f"ingress-manager add {domain} {ip} {port}" cmd = f"ingress-manager add {domain} {ip} {port}"

View file

@ -3,6 +3,8 @@ from .config import Config
from .dns import DNSManager from .dns import DNSManager
from .ingress import IngressManager from .ingress import IngressManager
from .router import RouterManager from .router import RouterManager
from .proxmox import ProxmoxManager
from .samba import SambaManager
import sys import sys
@click.group() @click.group()
@ -16,6 +18,61 @@ def cli(ctx, config):
click.echo(f"Error: {e}", err=True) click.echo(f"Error: {e}", err=True)
sys.exit(1) 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() @cli.group()
def dns(): def dns():
"""Manage DNS and DHCP""" """Manage DNS and DHCP"""

52
infra_cli/proxmox.py Normal file
View file

@ -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

42
infra_cli/samba.py Normal file
View file

@ -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

View file

@ -44,6 +44,29 @@ def test_ingress_cli(unique_id):
res = run_infra(["ingress", "remove", domain]) res = run_infra(["ingress", "remove", domain])
assert res.returncode == 0 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): def test_router_cli(unique_id):
name = f"Test-Cli-{unique_id}" name = f"Test-Cli-{unique_id}"
section = name.lower().replace("-", "_") section = name.lower().replace("-", "_")