feat: complete professional cli with full lifecycle and tests

This commit is contained in:
Fredrick Amnehagen 2026-02-05 11:37:29 +01:00
parent a7d97227d3
commit 34ba255024
12 changed files with 112 additions and 9 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -21,7 +21,8 @@ class DNSManager:
if res.returncode == 0:
raise ValueError(f"MAC {mac} already exists")
self.client.run(f"pct exec {self.lxc_id} -- sh -c \"echo 'dhcp-host={mac},{hostname},{ip}' >> {self.hosts_file}\")
cmd = f"sh -c \"echo 'dhcp-host={mac},{hostname},{ip}' >> {self.hosts_file}\""
self.exec_lxc(cmd)
self.reload()
def remove_host(self, mac):
@ -30,11 +31,13 @@ class DNSManager:
def add_dns(self, domain, ip):
self.exec_lxc(f"touch {self.dns_file}")
self.client.run(f"pct exec {self.lxc_id} -- sh -c \"echo 'address=/{domain}/{ip}' >> {self.dns_file}\")
cmd = f"sh -c \"echo 'address=/{domain}/{ip}' >> {self.dns_file}\""
self.exec_lxc(cmd)
self.reload()
def remove_dns(self, domain):
self.client.run(f"pct exec {self.lxc_id} -- sh -c \"sed -i '\#address=/{domain}/#d' {self.dns_file}\")
cmd = f"sh -c \"sed -i '\#address=/{domain}/#d' {self.dns_file}\""
self.exec_lxc(cmd)
self.reload()
def reload(self):
@ -47,4 +50,4 @@ class DNSManager:
def list(self):
hosts = self.exec_lxc(f"cat {self.hosts_file}").stdout
dns = self.exec_lxc(f"cat {self.dns_file}").stdout
return {"hosts": hosts, "dns": dns}
return {"hosts": hosts, "dns": dns}

View file

@ -63,11 +63,45 @@ def ingress_add(config, domain, ip, port, https):
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):
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)
mgr.remove_forward(section)
click.echo(f"Removed port forward {section}")
@router.command(name='list')
@click.pass_obj
def router_list(config):

View file

@ -38,16 +38,17 @@ class RouterManager:
def list(self):
# Get sections
# Use -n to prevent SSH from consuming stdin if we were in a loop (here we use split)
res = self.client.run("uci show firewall | grep '=redirect' | cut -d. -f2 | cut -d= -f1 | sort | uniq")
sections = res.stdout.strip().split('\n')
results = []
for section in sections:
if not section: continue
name = self.client.run(f"uci get firewall.{section}.name", capture=True).stdout.strip()
proto = self.client.run(f"uci get firewall.{section}.proto", capture=True).stdout.strip()
port = self.client.run(f"uci get firewall.{section}.src_dport", capture=True).stdout.strip()
dest = self.client.run(f"uci get firewall.{section}.dest_ip", capture=True).stdout.strip()
name = self.client.run(f"uci get firewall.{section}.name").stdout.strip()
proto = self.client.run(f"uci get firewall.{section}.proto").stdout.strip()
port = self.client.run(f"uci get firewall.{section}.src_dport").stdout.strip()
dest = self.client.run(f"uci get firewall.{section}.dest_ip").stdout.strip()
results.append({
"section": section,
"name": name,
@ -55,4 +56,4 @@ class RouterManager:
"port": port,
"dest": dest
})
return results
return results