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):
# 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)

View file

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

View file

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

View file

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

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])
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("-", "_")